Skip to content
Back to all concepts
AlgorithmsSpatial IndexingDatabases

Quadtrees (Location Based Services)

Spatial data structure recursively subdividing 2D space into four quadrants, supporting region searches with explicit limits for clustered points, geographic coordinates, and query size.

By ScaleWikiUpdated 3 min readAdvanced

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

typescript
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

typescript
// 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.

javascript
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:

FeatureQuadtreeGeohash
StorageIn-memory or persisted implementationIn-memory keys or database columns
Dynamic updatesFastSlower (reindex)
PrecisionCustomizableFixed levels
QueryDepends on depth and candidatesDepends on index and covered cells
ImplementationComplexSimple

Interview Tips 💡

When discussing quadtrees in system design interviews:

  1. Problem identification: "For Uber, need to find nearby drivers quickly..."
  2. Why not brute force: "Checking 1M drivers is O(N) → too slow for real-time"
  3. Quadtree explanation: "Divide map into regions, only search nearby quads ; measure candidate work"
  4. Capacity tuning: "Use capacity 10-50 depending on density. Cities need finer subdivision..."
  5. Dynamic updates: "Drivers move → rebuild quadtree every 30 seconds or use removal/insertion"
  6. Alternative: "Geohash simpler for static data, quadtree better for dynamic updates"
  7. Validate assumptions: "Test coincident points, region edges, and projected coordinate units."
  • 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.