Haversine Proximity + Severity-Gated Eligibility
Architectural Overview
Filtering facilities by severity eligibility before ranking by distance, using a plain Haversine calculation over an in-process cache, fast enough for the inline triage path, with the ranked candidate list already shaped for a future travel-time upgrade.
Every triage decision needs to answer one question fast: which facilities, out of MediCoord's full facility directory, can actually take this patient right now? This filter runs on every request, so it needs to be correct and cheap before it needs to be clever.
The Problem
A naive 'closest facility' query just sorts by distance. But distance alone can send a patient to a facility that's wrong for their condition: a routine-only clinic showing up ahead of a hospital that can actually treat an emergent case, or the reverse. Ranking has to happen after eligibility is decided, not before, and it has to run inside the latency budget of a chat response, with no room for a slow query across the full facility table on every message.
A facility 500m away that doesn't accept the patient's severity level isn't 'close'. It's not a candidate. Filtering has to run before distance sorting, not after.
Proximity search runs inline in the triage response path. It has to return in milliseconds, not seconds, or it becomes the slowest part of every chat turn.
The Architecture Strategy
find_nearest_facilities() does two things in a fixed order: filter, then sort. It reads the full facility list from an in-process cache, populated ahead of time from Supabase and not queried per-request, keeps only facilities where the requested severity appears in that facility's accepted_severity array, then ranks the survivors by Haversine great-circle distance, the standard spherical-law-of-cosines formula, with no external geo library. The top N (defaulting to 3, tunable by environment variable) go back to the caller, with the nearest becoming the recommended facility and the rest returned as alternatives. Distance is computed at request time, not pre-materialized, because the input point (the patient's current GPS coordinates) is different on every call; there's nothing to precompute.
def haversine_km(lat1, lng1, lat2, lng2) -> float:
R = 6371.0
φ1, φ2 = math.radians(lat1), math.radians(lat2)
Δφ = math.radians(lat2 - lat1)
Δλ = math.radians(lng2 - lng1)
a = math.sin(Δφ / 2) ** 2 + math.cos(φ1) * math.cos(φ2) * math.sin(Δλ / 2) ** 2
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def find_nearest_facilities(lat, lng, severity, top_n=None) -> list[dict] | None:
facilities, _ = get_cached_facilities()
if facilities is None:
return None
eligible = [f for f in facilities if severity in f.get("accepted_severity", [])]
if not eligible:
return []
ranked = sorted(
[{**f, "distanceKm": round(haversine_km(lat, lng, f["lat"], f["lng"]), 2)} for f in eligible],
key=lambda x: x["distanceKm"],
)
return ranked[:top_n or TOP_N_DEFAULT]Alternatives Considered
This is the more scalable answer, and it's already scoped as a follow-up: the function returns its full ranked candidate list specifically so a future re-rank by real travel time can happen without any backend change. We didn't reach for PostGIS first because the facility count in a single-city directory is small enough that an in-memory linear scan is faster to ship and easier to reason about than standing up a spatial index, tuning it, and keeping it in sync with a Supabase table that already changes on its own schedule.
Precomputing distances from every facility to a grid of coordinates would make lookups near-instant, but the input isn't a grid point. It's wherever the patient happens to be standing, at whatever precision their device reports. Precomputing against irregular real-world GPS input means either snapping to a grid, which adds error exactly where distance accuracy matters most, or recomputing anyway, which defeats the purpose.

Lessons Learned
find_nearest_facilities() returns None (not an empty list) when the facilities cache itself hasn't been populated yet, a real state during cold start or a Supabase outage. Early logic conflated that with 'zero facilities match this severity', which returns an empty list. Callers need to check for None explicitly: 'we don't have data yet' is not the same message to a patient as 'no facility currently accepts this severity.'
The result cap limits how many results come back, not how many exist. A severity with only one eligible facility in the whole directory correctly returns a list of one. The function doesn't pad or backfill with ineligible facilities to hit the cap.
Haversine gives straight-line distance, and straight-line distance is a known approximation of how long it actually takes to get somewhere: a facility 1.5km away across a highway can be slower to reach than one 4km away with a direct route. That gap is real and we're not hiding it: it's exactly the gap the roadmap item below closes. In the meantime, the eligibility filter (can this facility even take this patient) is correct today; only the ranking within the eligible set is an approximation. What's next: the function already returns its full ranked candidate list to the frontend for this reason. A composite score combining real travel time from a routing API with live queue depth can replace the Haversine sort without changing this function's contract, once that data exists.
- 1.21 km average routing error (Haversine vs. real driving distance) across 30 shadow-call samples, spread across 400 triage requests at varied Toronto coordinates.
- Sampled live shadow-call to Geoapify's Route Matrix API at a 10% rate, dispatched as a fire-and-forget background task after the response is already sent, never on the request's critical path.
- 400 triage requests fired via a ThreadPoolExecutor-based Python script against 12 scattered Toronto coordinates and eval test accounts — not a load-testing tool, request-volume for sample diversity, not throughput stress testing.
- 30 real Geoapify comparisons observed via the routing_shadow_error_km Prometheus summary metric (mean = sum/count, read directly off /metrics — no log scraping). Window: 2026-07-14, eval Supabase project.