SD System Design In Depth
0%
design system-designgeospatialnearby-search

Design: nearby search

Module 6.2: Interactive Product System Designs Updated Sep 18, 2026
📄 Cheat Sheet
Proximity Search & Geospatial Indexes Explained
Hello Interview Proximity Search & Geospatial Indexes Explained Watch on YouTube ↗

A focused explanation of geohash, quadtrees, S2/H3 and native spatial indexes, the exact comparison this lesson makes.

0:00 / --:--

The Two-Dimensional Spatial Indexing Problem

Two independent scalar B-tree indexes do not directly provide a nearest-neighbor search. An unindexed latitude/longitude bounding-box query may scan many rows, while databases with GiST, R-tree, geography, or other spatial indexes can serve this workload efficiently. The design choice is between those native spatial capabilities and an application-managed cell index—not SQL versus indexing.

Spatial Indexing Strategies

flowchart TD Coordinates["Latitude & Longitude"] --> Choice{"Indexing Algorithm"} Choice --> Geohash["Geohash: Base32 Z-order curve strings"] Choice --> QuadTree["QuadTree: Hierarchical 4-quadrant tree in memory"] Choice --> Cells["S2 quadrilateral cells / H3 mostly hexagonal cells"] Geohash --> KeyValue[("Redis Sorted Sets / NoSQL Prefix Query")] Cells --> Candidates[("Neighbor cells + exact distance filter")]

Geohash & Peano Space-Filling Curves

Geohashing recursively subdivides the globe into a grid and maps 2D coordinates into a 1D alphanumeric string using a Z-order space-filling curve. Locations sharing the same prefix are geographically proximate:

  • Precision 5: ~4.9 km × 4.9 km cell (city district scale)
  • Precision 6: ~1.2 km × 0.6 km cell (neighborhood scale)
  • Precision 7: ~152 m × 152 m cell (street block scale)

To find places within 1 kilometer, cover the search circle with every cell it intersects at the chosen precision, query those cells, and apply an exact spherical-distance filter. Center plus eight neighbors is a useful small-grid example, not a universal guarantee: geohash cell dimensions vary with precision and latitude, and a radius can span more cells.

Dynamic Drivers vs. Static Places

For static places (restaurants, ATMs in Yelp/Google Places), data is indexed into Geohash or QuadTree shards and heavily cached. For dynamic moving objects (Uber/Lyft drivers updating coordinates every 4 seconds), coordinates are written to an in-memory spatial index (like Redis Geospatial commands backed by sorted sets) to prevent database write saturation.

Worked example: 1 km search with geohash

  • The user is at latitude 40.7580, longitude -73.9855 (Times Square). At precision 6 a cell is about 1.2 km x 0.61 km at the equator; cells get narrower east to west as latitude increases.
  • A 1 km radius spans about two cells north to south and several east to west, so the covering set is roughly 9 to 15 cells, not always exactly 9.
  • Each cell lookup is a range scan on a geohash-prefixed key, in a Redis sorted set or a DB index. With Manhattan densities of 1,000 to 3,000 places per cell, you gather perhaps 20,000 candidates.
  • Exact haversine filtering of 20,000 points takes about 1 ms of CPU. Then sort by distance or rank, and return the top 20.
  • For sparse areas, widen progressively: precision 6, then 5, then 4, until at least K results are found.

Index options

IndexCell shapeAdapts to densityStrengthWeakness
GeohashRectangles on a Z-order curveNoPlain string prefixes; works in any KV store or Redis GEOEdge discontinuities; cell shape varies with latitude
QuadtreeSquares split recursivelyYes (split when a leaf exceeds N points, for example 100)Balanced load in dense and sparse areasUsually an in-memory structure; rebuilding it is costly for moving points
S2 (Google)Spherical quadrilaterals on a Hilbert curveMulti-level coveringsGood locality; region coverings for arbitrary shapesMore complex library
H3 (Uber)Hexagons, plus 12 pentagonsMulti-resolutionAll six neighbours are equidistant; good for aggregation and pricing zonesChildren do not exactly tile their parent
R-tree / GiST (PostGIS)Bounding boxesYesNative SQL; exact ST_DWithin queries with index supportScaling writes means sharding the database yourself

Static places vs moving objects

flowchart LR subgraph Static P[("Places DB with PostGIS or geohash column")] --> RC["Read replicas and a cache per cell"] end subgraph Moving D["Driver pings every 4 s"] --> G["Location service sharded by city"] G --> M[("In-memory geo index, TTL 30 s")] end Q["Nearby query"] --> RC Q --> M

Yelp-style place search is read-heavy and changes slowly, so cache the results for each cell for minutes. Driver search is write-heavy and every result is stale within seconds. Expire positions that have not been refreshed recently (for example after 30 s), so a phone that goes offline does not keep appearing as an available driver.

Check yourself Question 1 of 3
Covering the radius recall
From: Geohash & Peano Space-Filling Curves

A café 20 m away from the user sits just across a geohash cell boundary and has a completely different prefix. How does a correct 1 km search still find it?

Key Takeaways

Avoid unindexed 2D scans; use a database spatial index or a cell-based index selected for the query and update workload.
Geohash converts 2D coordinates into 1D prefix-searchable strings using Z-order curves.
Cover all cells intersecting the requested radius, then exact-distance filter candidates; eight neighbors are not sufficient for every radius, precision, or latitude.
Decouple high-frequency moving objects (drivers) in Redis Geospatial from static location catalogs (restaurants).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue