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_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 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 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 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 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 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 asyncio
import json import json
import math
import re import re
from typing import Optional from typing import Optional
from app.internal.logger import logger from app.internal.logger import logger
@@ -273,14 +274,25 @@ async def extract_scenes(
return processed 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( async def _geocode_location(
location_str: str, node_lat: float, node_lon: float location_str: str, node_lat: float, node_lon: float
) -> Optional[dict]: ) -> Optional[dict]:
""" """
Geocode a location string using Nominatim, biased toward the node's area. 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 import httpx
from app.config import settings
viewbox = ( viewbox = (
f"{node_lon - _GEO_DELTA},{node_lat - _GEO_DELTA}," f"{node_lon - _GEO_DELTA},{node_lat - _GEO_DELTA},"
@@ -304,8 +316,17 @@ async def _geocode_location(
r.raise_for_status() r.raise_for_status()
results = r.json() results = r.json()
if results: if results:
coords = {"lat": float(results[0]["lat"]), "lng": float(results[0]["lon"])} lat = float(results[0]["lat"])
logger.info(f"Geocoded '{location_str}'{coords}") 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 return coords
except Exception as e: except Exception as e:
logger.warning(f"Geocoding failed for '{location_str}': {e}") logger.warning(f"Geocoding failed for '{location_str}': {e}")