Maps proximity / POI search system design (Yelp, Google Maps "nearby restaurants"): H3-indexed POI search with k-ring scan, hot-query cache, vector tile delivery, and OSRM-style routing. Includes 5 scenarios (nearby search, expand radius, hot tourist area, route A->B, POI ingest) and 2 ADRs (geohash vs H3 vs quadtree, vector vs raster tiles).
Maps proximity service answers questions like “restaurants near me”, “nearest available driver”, “gas stations within 5 km”, or “POIs in this viewport”. It looks like a normal search problem, but the defining dimension is geography. The system must combine spatial indexing, hot-query caching, ranking, map tiles, updates, and sometimes live moving objects.
At ride-hailing or large maps scale, assume millions of active moving objects, location updates every few seconds, hundreds of thousands of proximity queries per second, and a global POI catalog with hundreds of millions of places. A driver-location backend may receive over a million writes per second during peak. A POI search backend may mostly serve reads, but it must handle hot tourist areas, map viewport bursts, and boundary cases where the radius crosses cells.
This case trains ::concept{slug="partitioning-strategies"}, ::concept{slug="caching-strategies"}, spatial indexes, H3/geohash thinking, query fan-out, and the difference between static POI search and live location matching.
Convert latitude and longitude into cells. Instead of scanning all places or all drivers by distance, map every object to a spatial cell and query the target cell plus neighboring cells. Then compute exact distance only for the small candidate set. The index narrows the search; the final distance calculation and ranker decide the result.
For static POIs, the source of truth can be PostGIS or another geospatial database, while Redis or an H3-indexed serving layer provides hot read performance. For moving drivers, the current location index is usually in memory, partitioned by region or parent cell, with a durable append-only log for history and debugging.
The second mental model is that map rendering is not proximity search. Vector tiles, raster tiles, routing graphs, POI search, and live driver matching are related but separate workloads. A good design keeps them separated so a tile-rendering spike does not break ride matching.
The diagram shows a user or rider app reaching an API Gateway, then a proximity Search Service or Match API. The search side uses an H3 cell index, a query cache, a ranker, and a POI catalog. The live-location side ingests driver pings through Edge LB into Kafka partitioned by H3 parent cell, then updates Geo shards and Redis/H3 indexes. A Coordinator fans out k-nearest-neighbor queries to the relevant shards and merges results.
The tile path goes through CDN and a vector tile renderer. Routing is represented separately through an OSRM or Valhalla-style service backed by road graph data. This separation is important: nearby search asks “what is near this point”, routing asks “how do I travel between two points”, and tiles ask “what map geometry should I draw”.
The diagram also captures the ADR between geohash, H3, and quadtree. H3 is attractive because hex cells have more uniform neighbors and gridDisk/k-ring style lookup makes radius expansion predictable. Geohash is simple and storage-friendly but has awkward boundary and unequal-cell issues. Quadtree is useful for adaptive density and polygon work, but can be harder to serve at very high QPS.
Nearby search teaches the basic path. The user asks for restaurants around a coordinate. The service maps the coordinate to an H3 cell, reads the cell and neighbors, filters by category and open status, computes exact distances, applies ranking by distance/rating/popularity/context, and returns the top results. The lesson is that approximate cell lookup must be followed by exact filtering.
Expand-radius scenario teaches graceful degradation. If the first cell ring returns too few candidates, the service expands to a larger k-ring or larger radius. The query should return enough useful results without scanning the whole city. Pagination must be stable enough that users do not see duplicates or missing items as the radius changes.
Hot tourist area scenario teaches caching and load splitting. Queries for “restaurants near Times Square” or a famous landmark repeat constantly. A short TTL cache by H3 cell, filters, and locale can absorb bursts. For live drivers, hot cells may need sub-sharding or scatter by driver ID because one H3 cell can contain too many objects during an event.
Boundary-query scenario teaches why neighbor lookup matters. If a user stands near a cell edge, the nearest object may be in the adjacent cell. A design that queries only one cell will produce visibly wrong answers. Querying the target cell plus neighbors and then sorting by exact distance fixes this.
Routing scenario teaches separation of concerns. Nearby POI search can use H3 and Redis; route ETA needs a road network graph, turn restrictions, traffic, and contraction hierarchies or similar acceleration. Straight-line distance is not travel time.
POI ingest teaches freshness and validation. Business updates, new reviews, closures, spam edits, and category changes enter an ingestion pipeline, update the source database, rebuild or patch the serving index, and invalidate affected caches.
H3 vs geohash vs quadtree is the core spatial trade-off. H3 gives uniform hex neighbor behavior and hierarchy, which is convenient for k-ring scans and hot-cell rollups. Geohash uses string prefixes and is easy to shard, but rectangular cells and boundary cases make radius search messy. Quadtree adapts well to density but can become pointer-heavy and operationally complex.
Redis Geo vs custom H3 sets is another trade-off. Redis Geo supports simple radius queries, but at very high scale you may want explicit cell membership sets, compact binary payloads, and custom partitioning. Many production systems use a hybrid: Redis for current hot state, durable logs for replay, and database indexes for source-of-truth POIs.
Freshness vs cost appears everywhere. Driver location needs freshness measured in seconds. POI category or rating updates can tolerate minutes. Map tiles can be cached for days. Treating every object as equally fresh wastes money; treating live drivers like static POIs breaks matching.
Vector tiles vs raster tiles affect client and CDN trade-offs. Vector tiles reduce bandwidth and allow client-side styling, dark mode, labels, and overlays, but require more client CPU/GPU. Raster tiles are cheap to display and support old clients, but restyling and localization are expensive.
Uber popularized H3 and uses cell-based thinking for dispatch, pricing, maps, and marketplace balancing. Google Maps, Apple Maps, Yelp, Foursquare, DoorDash, Bolt, and delivery platforms all rely on combinations of spatial indexes, ranking, caching, and routing. PostGIS is common for authoritative geo storage and polygon queries. Elasticsearch/OpenSearch can serve geo-distance filters for POI search. OSRM and Valhalla are common open-source routing engines. Mapbox vector tiles define a widely used delivery format for map rendering.
Ride-hailing adds moving-object freshness and availability state. Restaurant or POI search adds catalog quality, moderation, opening hours, ranking, and reviews. Logistics adds vehicle constraints, depot routing, and ETA accuracy. The same spatial foundation appears in different products.
Do not compute distance against every POI or every driver. Even if it works in a small city, it will fail globally and under bursty traffic.
Do not query only the current cell. Boundary errors are user-visible: the nearest result can be just across the cell border. Always query neighboring cells or use an index that handles boundary expansion.
Do not use straight-line distance as ETA. For walking it may be acceptable as a rough sort key; for driving, rivers, one-way streets, highways, and traffic dominate.
Do not use one global shard key for live locations. Partition by spatial parent cell or region so writes and reads stay local. Watch for hot cells and support split strategies.
Do not put map tile rendering, proximity search, and routing into one service. Their caching, CPU profile, data model, and failure modes are different.
Do not cache live driver results too long. Returning a driver who moved away or became unavailable hurts dispatch quality. Use freshness flags and short TTLs for moving entities.
Do not build this architecture for a small local directory with thousands of places and low traffic. A relational database with PostGIS and a simple bounding-box plus distance query may be enough.
Do not use H3-only indexing for complex polygon containment, cadastral maps, or legal boundary calculations where exact geometry matters. Use PostGIS or a geometry engine for final correctness.
Do not use real-time driver-location infrastructure for static POI search. Static places do not need Kafka pings, hot in-memory movement indexes, or second-level freshness.
Do not use proximity as the only ranking signal when quality matters. Nearest restaurant, nearest hospital, and nearest driver have different product semantics. Availability, rating, ETA, capacity, safety, and business rules often matter more than raw distance.
Read ::concept{slug="partitioning-strategies"} for spatial sharding and hot-cell handling, ::concept{slug="caching-strategies"} for hot query and tile caches, ::concept{slug="search-engine"} for retrieval plus ranking, ::concept{slug="stream-processing"} for live location ingestion, and ::concept{slug="pacelc-theorem"} for the latency/freshness trade-off. Then compare this case with ride matching, food delivery dispatch, and hotel search: all use geography, but each has different correctness and freshness requirements.