Pattern · Geospatial
Proximity Search
Efficient 2D spatial indexing for geospatial radius and nearest-neighbor queries. Geohashes, hierarchical quadtrees, Google S2 and Uber H3 hexagonal cells, PostGIS R-trees, Redis GEO, moving driver location updates, and nearby places retrieval.
11 min read
Geospatial proximity search answers two classic system design questions: "Which restaurants are within a 3 km radius of my current location?" (Yelp, Google Maps) and "Which 10 available drivers are closest to this passenger?" (Uber, Lyft).
Querying coordinates with standard relational indexes is inefficient because physical geographic space is two-dimensional, whereas standard database indexes order data along a single dimension. Spatial indexing patterns map 2D spherical coordinates into 1D linear strings or integers, enabling sub-millisecond range and bounding-box lookups across millions of locations.
Why standard B-Trees fail on 2D coordinates
A naive approach stores places in a relational table with separate latitude and longitude columns:
SELECT id, name FROM places
WHERE latitude BETWEEN 37.70 AND 37.80
AND longitude BETWEEN -122.50 AND -122.40;A standard composite B-Tree index on (latitude, longitude) sorts primarily by latitude. The database uses the index to filter the narrow latitude slice, but within that slice, it must perform an exhaustive scan across all longitude values. As table sizes grow to millions of points, 2D range scans degrade into high-latency disk reads.
Geohash: Interleaving latitude and longitude
A Geohash converts a (latitude, longitude) coordinate pair into a short alphanumeric string (Base32) by recursively dividing the world into halves and interleaving their binary bits:
- Binary subdivision: Alternate between dividing longitude (East/West) and latitude (North/South). Assign bit
1if the coordinate falls in the upper half, and bit0if in the lower half. - Interleaving: Interleave the bits into a single continuous bit sequence:
lon[0], lat[0], lon[1], lat[1], .... - Base32 encoding: Group every 5 bits and map them to a Base32 character (
0-9, b-z, omittinga, i, l, oto prevent reading errors).
Because of bit interleaving, points that share a long Geohash prefix are physically close to each other on Earth:
9q8yy (precision 5, ~4.9 km x 4.9 km cell)
9q8yyk (precision 6, ~1.2 km x 0.6 km cell)
9q8yyk8 (precision 7, ~150 m x 150 m cell)To find all restaurants near a user, compute the user's 6-character Geohash cell and query standard database indexes using prefix matching:
SELECT * FROM places WHERE geohash LIKE '9q8yyk%';Quadtrees: Adaptive spatial partitioning
While Geohash creates fixed grid boundaries, real-world populations are unevenly distributed: downtown Manhattan contains millions of venues, while rural farmland contains very few.
A Quadtree is an in-memory 2D tree where every internal node splits into exactly four children (North-West, North-East, South-West, South-East):
- Start with the root node representing the entire geographic region.
- When the number of places in a node exceeds a capacity threshold (e.g. 500 items), split the node into 4 quadrant children and distribute the items down.
- Dense urban neighborhoods recursively subdivide into tiny blocks, while sparse rural regions remain single large nodes.
- Radius queries traverse down tree branches that intersect the search circle, visiting only relevant nodes.
Quadtrees require keeping the tree structure in memory, making them ideal for read-heavy venue searches with periodic batch re-indexing.
Hexagonal global grids: Uber H3 and Google S2
Modern mobility platforms have largely transitioned from square Geohashes to hierarchical hexagonal discrete global grids like Uber H3:
Square Grids (Geohash)
- Easy string prefix matching in SQL B-Trees
- Human-readable characters
- Two types of neighbors: 4 edge neighbors (distance d) and 4 corner diagonal neighbors (distance d * sqrt(2))
- Distorts distance calculations at cell edges
Pick it when: Relational database indexing, URL location tags.
Hexagonal Grids (Uber H3)
- Every hexagon has exactly 6 identical equidistant neighbors
- Uniform neighbor distance simplifies radius expansion, dispatching, and dynamic surge pricing algorithms
- Hexagons cannot be divided into exact smaller hexagons (uses nested approximations)
Pick it when: Ride-hailing driver dispatch, logistics, food delivery supply/demand modeling.
Spherical Hilbert Grids (Google S2)
- Projects Earth onto a cube with Hilbert space-filling curves
- Maintains optimal 1D locality
- 64-bit cell integer representation
- Requires specialized library bindings
Pick it when: High-performance geospatial radius queries and polygon intersections.
Handling fast-moving objects: Ride-hailing architectures
In ride-hailing (Uber, Lyft), thousands of drivers transmit GPS pings every 3 to 5 seconds. This creates a massive write-heavy workload that would destroy a disk-based relational database.
Production architectures separate static locations from dynamic locations:
- Static places (Restaurants, Hotels): Stored in PostgreSQL with PostGIS (R-Tree indexes). Infrequent writes, rich metadata filters (cuisine, pricing, rating).
- Dynamic movers (Drivers, Couriers): Stored in an in-memory datastore like Redis using geospatial sorted sets.
- Redis GEO commands: Drivers update locations via
GEOADD drivers:sf <lon> <lat> <driver_id>. Finding nearby drivers is a single in-memory command:GEORADIUS drivers:sf <lon> <lat> 3 km WITHDIST COUNT 10 ASC. - Sharding by city / region: Partition Redis clusters by city or coarse H3 resolution (e.g. London, NYC, SF) because drivers in San Francisco never match passengers in London.
Numbers to quote
- Geohash precision 5
- ~4.9 km x 4.9 km (city district level)
- Geohash precision 6
- ~1.2 km x 0.6 km (neighborhood level)
- Geohash precision 7
- ~150 m x 150 m (street block level)
- Redis GEOADD throughput
- 50,000 to 100,000 location updates/sec per Redis node
- Driver ping frequency
- Every 3 to 5 seconds while active
- H3 resolution 8
- ~0.7 sq km average area (ideal for ride dispatch)
Where it bites
- Polar and 180th meridian edge anomalies: Coordinate calculations near the international date line (-180 / +180 degrees longitude) wrap around, creating mathematical discontinuities in naive bounding box formulas.
- Altitude blindness: 2D coordinate projections ignore altitude. A driver on a highway overpass and a passenger on the street directly below share the exact same 2D coordinates but cannot reach each other.
- In-memory quadtree rebalancing: If mobile drivers move into a concentrated event venue (like a football stadium exiting), an in-memory quadtree undergoes continuous node splitting and rebalancing, causing latency spikes. Use fixed-resolution H3 or Redis GEO instead of mutable quadtrees for moving entities.
Try it
Nearby Places Search HLD
Design a location platform serving Yelp-style nearby restaurant queries and real-time ride-hailing driver dispatch.