Finding Nearby
The problem: How does Uber find all drivers within 1km of you? How does Yelp show restaurants nearby?
Naive solution: Check distance to EVERY driver/restaurant → O(N) → Too slow!
Quadtree approach: Skip regions that cannot intersect the query. Performance depends on point distribution, depth, and result size; the worst case can still scan most data.
What is a Quadtree?
A Quadtree is a tree data structure where each internal node has exactly four children, representing the four quadrants of a 2D space.
┌─────────────────┐
│ NW │ NE │
│ │ │
├─────┼───────────┤
│ │ │ │
│ SW │ SE1 │ SE2 │
│ ├─────┼─────┤
│ │ SE3 │ SE4 │
└─────┴─────┴─────┘
Tree structure:
Root
/ | \ \
NW NE SW SE
/ | \ \
SE1 SE2 SE3 SE4
Key idea: Recursively split when too many points in one region.
How Quadtrees Work
Insertion Algorithm
1. Start at root (entire map)
2. Which quadrant does the point belong to?
3. If quadrant has < MAX_CAPACITY points:
Add point
Else:
Subdivide quadrant into 4 sub-quadrants
Redistribute existing points
Add new point
Search Algorithm
To find all points within radius R of point P: 1. Start at root 2. For each quadrant: - Does circle (P, R) intersect this quadrant? - Yes: Recursively search this quadrant - No: Skip this quadrant entirely 3. Return all points found
Implementation
interface Point {
x: number;
y: number;
data?: any; // Driver ID, restaurant info, etc.
}
interface Rectangle {
x: number; // Center x
y: number; // Center y
width: number; // Half-width
height: number; // Half-height
}
class Quadtree {
private boundary: Rectangle;
private capacity: number;
private points: Point[] = [];
private divided: boolean = false;
// Child quadrants
private northwest?: Quadtree;
private northeast?: Quadtree;
private southwest?: Quadtree;
private southeast?: Quadtree;
constructor(boundary: Rectangle, capacity: number = 4, private depth = 0, private maxDepth = 24) {
if (!Number.isInteger(capacity) || capacity < 1 || !Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 32) throw new Error("Invalid capacity or maximum depth");
if (![boundary.x, boundary.y, boundary.width, boundary.height].every(Number.isFinite) || boundary.width <= 0 || boundary.height <= 0) throw new Error("Invalid boundary");
this.boundary = boundary;
this.capacity = capacity;
}
// Insert point into quadtree
insert(point: Point): boolean {
// Point not in boundary
if (!Number.isFinite(point.x) || !Number.isFinite(point.y) || !this.contains(this.boundary, point)) {
return false;
}
// Space available in this quad
if (!this.divided && (this.points.length < this.capacity || this.depth >= this.maxDepth)) {
this.points.push(point);
return true;
}
// Need to subdivide
if (!this.divided) {
this.subdivide();
}
// Insert into appropriate child
return (
this.northwest!.insert(point) ||
this.northeast!.insert(point) ||
this.southwest!.insert(point) ||
this.southeast!.insert(point)
);
}
// Subdivide into 4 quadrants
private subdivide(): void {
const { x, y, width, height } = this.boundary;
const halfWidth = width / 2;
const halfHeight = height / 2;
// Create 4 child quadrants
this.northwest = new Quadtree(
{ x: x - halfWidth, y: y - halfHeight, width: halfWidth, height: halfHeight },
this.capacity, this.depth + 1, this.maxDepth
);
this.northeast = new Quadtree(
{ x: x + halfWidth, y: y - halfHeight, width: halfWidth, height: halfHeight },
this.capacity, this.depth + 1, this.maxDepth
);
this.southwest = new Quadtree(
{ x: x - halfWidth, y: y + halfHeight, width: halfWidth, height: halfHeight },
this.capacity, this.depth + 1, this.maxDepth
);
this.southeast = new Quadtree(
{ x: x + halfWidth, y: y + halfHeight, width: halfWidth, height: halfHeight },
this.capacity, this.depth + 1, this.maxDepth
);
this.divided = true;
// Redistribute existing points to children
for (const point of this.points) {
this.northwest.insert(point) ||
this.northeast.insert(point) ||
this.southwest.insert(point) ||
this.southeast.insert(point);
}
// Clear points from parent (now in children)
this.points = [];
}
// Query: find all points within range
query(range: Rectangle, found: Point[] = []): Point[] {
// Range doesn't intersect this quad
if (!this.intersects(this.boundary, range)) {
return found;
}
// Check points in this quad
for (const point of this.points) {
if (this.contains(range, point)) {
found.push(point);
}
}
// Recursively check children
if (this.divided) {
this.northwest!.query(range, found);
this.northeast!.query(range, found);
this.southwest!.query(range, found);
this.southeast!.query(range, found);
}
return found;
}
// Query: find all points within radius
queryRadius(center: Point, radius: number): Point[] {
if (![center.x, center.y, radius].every(Number.isFinite) || radius < 0) throw new Error("Invalid centre or radius");
const found: Point[] = [];
// Create bounding rectangle for circle
const range: Rectangle = {
x: center.x,
y: center.y,
width: radius,
height: radius
};
const candidates = this.query(range);
// Filter by actual distance
for (const point of candidates) {
const dist = this.distance(center, point);
if (dist <= radius) {
found.push(point);
}
}
return found;
}
// Helper: Check if rectangle contains point
private contains(rect: Rectangle, point: Point): boolean {
return (
point.x >= rect.x - rect.width &&
point.x <= rect.x + rect.width &&
point.y >= rect.y - rect.height &&
point.y <= rect.y + rect.height
);
}
// Helper: Check if two rectangles intersect
private intersects(r1: Rectangle, r2: Rectangle): boolean {
return !(
r1.x + r1.width < r2.x - r2.width ||
r1.x - r1.width > r2.x + r2.width ||
r1.y + r1.height < r2.y - r2.height ||
r1.y - r1.height > r2.y + r2.height
);
}
// Helper: Euclidean distance
private distance(p1: Point, p2: Point): number {
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
Usage example: planar driver coordinates
// Create quadtree for San Francisco
// Boundaries: lat/long converted to x/y
const sfMap = new Quadtree({
x: 0, // Center x (simplified)
y: 0, // Center y
width: 10000, // 10km radius
height: 10000
}, 10); // Max 10 drivers per quad
// Add drivers
const drivers = [
{ x: 100, y: 200, data: { id: 'driver1', name: 'John' } },
{ x: 150, y: 250, data: { id: 'driver2', name: 'Jane' } },
{ x: 5000, y: 5000, data: { id: 'driver3', name: 'Bob' } },
// ... 10,000 more drivers
];
for (const driver of drivers) {
sfMap.insert(driver);
}
// User requests ride
const user = { x: 120, y: 220 };
const radius = 1000; // 1km
// Search intersecting regions, then filter candidates by distance
const nearbyDrivers = sfMap.queryRadius(user, radius);
console.log(`Found ${nearbyDrivers.length} drivers within 1km`);
// The number of candidates depends on their distribution and the radius.
Complexity and coordinate assumptions
This teaching implementation indexes points in a planar coordinate system. A radius of 1,000 means 1,000 of the same units as x/y. Latitude/longitude degrees are not metres: use an appropriate local projection or a spherical geospatial index for geographic distances.
Maximum depth prevents identical or extremely close points from causing endless subdivision. A leaf at that limit can exceed the nominal capacity, so a dense cluster can still require a linear scan. Query every intersecting cell rather than assuming a fixed eight-neighbour layout: quadtree cells can have different depths.
For moving objects, removal and reinsertion must preserve identity and avoid leaving stale positions behind. Those operations are not implemented above. A safe initial design can build a new tree from a consistent location snapshot, then swap it into use; choose the update rate from the application's freshness requirement.
A useful stress test
Insert a thousand points at exactly the same coordinate. The tree should terminate and a zero-radius query should return all thousand. Next place points on quadrant edges and compare range-query results with a brute-force scan. Finally use a radius covering the whole tree; no spatial index can avoid returning the entire matching result set.
Geohashing: Alternative Approach
Geohash converts lat/long to a string where nearby points share prefixes.
function geohash(lat, lng, precision = 8) {
// Example: San Francisco
// geohash(37.7749, -122.4194, 8) => "9q8yyk8y"
}
// Nearby search
const myHash = "9q8yyk8y";
const prefix = myHash.substring(0, 6); // "9q8yyk"
// SQL query
SELECT * FROM drivers
WHERE geohash LIKE '9q8yyk%'
-- All drivers with same prefix are nearby!
Comparison:
| Feature | Quadtree | Geohash |
|---|---|---|
| Storage | In-memory or persisted implementation | In-memory keys or database columns |
| Dynamic updates | Fast | Slower (reindex) |
| Precision | Customizable | Fixed levels |
| Query | Depends on depth and candidates | Depends on index and covered cells |
| Implementation | Complex | Simple |
Interview Tips 💡
When discussing quadtrees in system design interviews:
- Problem identification: "For Uber, need to find nearby drivers quickly..."
- Why not brute force: "Checking 1M drivers is O(N) → too slow for real-time"
- Quadtree explanation: "Divide map into regions, only search nearby quads ; measure candidate work"
- Capacity tuning: "Use capacity 10-50 depending on density. Cities need finer subdivision..."
- Dynamic updates: "Drivers move → rebuild quadtree every 30 seconds or use removal/insertion"
- Alternative: "Geohash simpler for static data, quadtree better for dynamic updates"
- Validate assumptions: "Test coincident points, region edges, and projected coordinate units."
Related Concepts
- Geohashing — Alternative spatial indexing
- R-Trees — 3D spatial indexing
- KD-Trees — K-dimensional search trees
- Spatial Databases — PostGIS, MongoDB geo queries
- Location-Based Services — Architecture patterns
Primary reference
D3 quadtree documentation describes a maintained implementation, including coincident points and range visitation. Use it to compare the responsibilities omitted by a teaching example. Related background: geohashing and database indexing.
Keep reading
Cache Eviction Policies
When the cache is full, something has to go. A comprehensive guide to LRU, LFU, ARC, and other replacement algorithms with implementation details.
CAP Theorem
Reason about a network partition using a reservation example, quorum limits and operation-specific consistency choices.
Geohashing (Location Encoding)
A geocoding system that encodes latitude/longitude coordinates into short alphanumeric strings for efficient proximity searches and spatial indexing.
HyperLogLog (Cardinality Estimation)
A probabilistic algorithm for counting unique items in massive datasets using minimal memory, with tunable statistical error and compact mergeable state.
Leader Election
Comprehensive guide to distributed leader election algorithms including Raft, Paxos, and Bully algorithm, covering consensus, split-brain prevention, and real-world implementations in Kubernetes, ZooKeeper, and etcd.
MapReduce
A programming model for processing massive datasets in parallel across distributed clusters. Understanding Map, Shuffle, Reduce with real-world use cases from Google, Hadoop, and Spark.