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:
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user