fix: reject geocode results outside node jurisdiction

Nominatim's viewbox is advisory (bounded=0), so ambiguous place names like
"Pinebrook" can resolve to locations 30-40km away in the wrong town. Added
a post-geocode distance gate: results farther than geocode_max_km (default
25km) from the node are discarded with a warning log rather than written to
the incident.

Also logs distance on successful geocodes for easier audit.

New config setting: geocode_max_km (float, default 25.0)
This commit is contained in:
Logan
2026-05-25 13:09:10 -04:00
parent 4b7d9dd49a
commit 0db09d6bf7
2 changed files with 25 additions and 3 deletions
+1
View File
@@ -29,6 +29,7 @@ class Settings(BaseSettings):
embedding_no_location_threshold: float = 0.97 # slow-path: match without location (very high bar)
embedding_cross_tg_threshold: float = 0.85 # cross-TG path: same dept + 2+ shared units
location_proximity_km: float = 0.5 # radius for location-proximity matching
geocode_max_km: float = 25.0 # reject geocode results farther than this from the node
incident_auto_resolve_minutes: int = 90 # auto-resolve after N minutes with no new calls
recorrelation_scan_minutes: int = 60 # re-examine orphaned calls ended within this window
tg_fast_path_idle_minutes: int = 90 # fast path: max minutes since incident last updated
+24 -3
View File
@@ -10,6 +10,7 @@ Falls back gracefully if the API is unavailable or returns malformed output.
"""
import asyncio
import json
import math
import re
from typing import Optional
from app.internal.logger import logger
@@ -273,14 +274,25 @@ async def extract_scenes(
return processed
def _geo_dist_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Haversine distance in km between two lat/lon points."""
R = 6371.0
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = math.sin(dlat / 2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2
return R * 2 * math.asin(math.sqrt(a))
async def _geocode_location(
location_str: str, node_lat: float, node_lon: float
) -> Optional[dict]:
"""
Geocode a location string using Nominatim, biased toward the node's area.
Returns {"lat": float, "lng": float} or None if geocoding fails.
Returns {"lat": float, "lng": float} or None if geocoding fails or the
result is farther than geocode_max_km from the node (wrong-jurisdiction guard).
"""
import httpx
from app.config import settings
viewbox = (
f"{node_lon - _GEO_DELTA},{node_lat - _GEO_DELTA},"
@@ -304,8 +316,17 @@ async def _geocode_location(
r.raise_for_status()
results = r.json()
if results:
coords = {"lat": float(results[0]["lat"]), "lng": float(results[0]["lon"])}
logger.info(f"Geocoded '{location_str}'{coords}")
lat = float(results[0]["lat"])
lng = float(results[0]["lon"])
dist_km = _geo_dist_km(node_lat, node_lon, lat, lng)
if dist_km > settings.geocode_max_km:
logger.warning(
f"Geocoding rejected '{location_str}' → ({lat:.4f}, {lng:.4f}) "
f"{dist_km:.1f}km from node exceeds geocode_max_km={settings.geocode_max_km}"
)
return None
coords = {"lat": lat, "lng": lng}
logger.info(f"Geocoded '{location_str}'{coords} ({dist_km:.1f}km from node)")
return coords
except Exception as e:
logger.warning(f"Geocoding failed for '{location_str}': {e}")