File: proximity-service/proximity_service.py

Date: 2026-06-05

Time: 13:58

proximity-service/proximity_service.py

Purpose

This file implements a proximity service — the backend for "find nearby businesses/places" queries (think Yelp, Google Maps "restaurants near me"). It owns spatial indexing and nearest-neighbor search over a set of Points of Interest (POIs), providing two alternative index strategies: geohash and quadtree. This is a classic system design interview problem: given millions of POIs and a user's coordinates, return the closest ones within a radius efficiently.

The file is self-contained — no external dependencies beyond stdlib — making it a reference implementation that demonstrates the core algorithms without infrastructure noise.

Key Components

POI (data class)

Simple value object holding id, name, lat, lon, category. No validation — it trusts callers to provide valid WGS-84 coordinates. This is the universal currency type that both indexes operate on.

haversine(lat1, lon1, lat2, lon2) -> float

Computes great-circle distance in kilometers using the haversine formula. This is the ground truth distance function — both indexes use it as the final filter after their spatial pruning narrows candidates. Earth radius is hardcoded to 6371 km.

GeohashIndex

A geohash-based spatial index that maps the 2D coordinate space to 1D strings using a space-filling curve.

Storage: Two dictionaries — pois maps id → (POI, geohash) for O(1) removal, and index maps geohash → {id: POI} for spatial lookup.

Key methods:

Precision table: Maps search radius to geohash precision. A 5 km search uses precision 5 (cells ~5 km wide); a 1 km search uses precision 6 (~1.2 km cells). This ensures the center cell + neighbors always covers the search circle.

_QuadNode (internal)

The recursive building block of the quadtree. Uses _slots_ for memory efficiency. Each node either holds points (leaf) or has 4 children (internal). Children are ordered NW, NE, SW, SE.

Key methods:

Quadtree

Public wrapper around _QuadNode. Default bounds cover the whole globe.

Key method: queryrange(lat, lon, radiuskm) — Converts the circular search area to a bounding box (approximating dlat and dlon from the radius), runs query_bbox, then filters candidates by exact haversine distance. The longitude approximation accounts for latitude-dependent distortion via cos(lat).

Patterns

1. Two-phase search (coarse → fine): Both indexes share the same strategy — use spatial structure to get a rough candidate set cheaply, then filter with exact haversine. This is the standard pattern for spatial search and avoids computing haversine against every POI.

2. Strategy pattern (implicit): GeohashIndex and Quadtree are interchangeable spatial indexes with similar APIs (add/insert, nearby/query_range). A production service would pick one based on access patterns — geohash for database-friendly prefix queries, quadtree for in-memory with uneven distribution.

3. Adaptive precision: GeohashIndex.nearby dynamically coarsens precision for larger radii. This is critical — a fixed-precision geohash would either miss results (too fine) or scan too many cells (too coarse).

4. Recursive spatial decomposition: The quadtree uses the classic recursive subdivision pattern with a capacity threshold (maxpoints) and depth limit (maxdepth) to prevent infinite recursion on coincident points.

Dependencies

Imports: math (trig for haversine, radians conversion, cos for longitude scaling) and collections.defaultdict (geohash index bucketing). No third-party dependencies.

Imported by: testproximityservice.py — the test suite is the only consumer, confirming this is a standalone reference implementation.

Flow

GeohashIndex query flow

1. nearby() called with (lat, lon, radius_km)

2. precisionfor_radius() walks the precision table to find the coarsest precision that still covers the radius

3. encode() converts the query point to a geohash at that precision

4. neighbors() decodes the center cell, steps in 8 directions, re-encodes to get adjacent cells

5. For each of the 9 cells (center + 8 neighbors), scan _index for all geohashes matching the prefix

6. Compute haversine distance for each candidate, keep those within radius_km

7. Sort by distance, truncate to limit

Quadtree query flow

1. queryrange() called with (lat, lon, radiuskm)

2. Radius converted to a lat/lon bounding box using 111 km/degree approximation

3. query_bbox() recursively traverses the tree, pruning nodes whose bounds don't intersect the box

4. Leaf nodes check each point against the bounding box

5. Candidates filtered by exact haversine distance

6. Sort by distance, return all matches (no limit parameter)

Invariants

Error Handling

Essentially none — this is a reference implementation that trusts its inputs:

Topics to Explore

Beliefs