diff --git a/drb-c2-core/app/config.py b/drb-c2-core/app/config.py index 265fa63..272c5fe 100644 --- a/drb-c2-core/app/config.py +++ b/drb-c2-core/app/config.py @@ -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 diff --git a/drb-c2-core/app/internal/intelligence.py b/drb-c2-core/app/internal/intelligence.py index e5c1624..87c42a4 100644 --- a/drb-c2-core/app/internal/intelligence.py +++ b/drb-c2-core/app/internal/intelligence.py @@ -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}")