Backend: _geocode_location now returns {lat, lng, location_type}, so the
precision rides onto the call doc and, unchanged via _resolve_location_pair,
onto the incident pin. Display-only: every correlator read indexes lat/lng.
Frontend: pins styled by precision (rooftop / interpolated ring /
GEOMETRIC_CENTER dashed uncertainty circle labelled with the spoken place /
unverified pre-#23 pins ghosted). liveWall.pathStops() is shared by the map
and the call spine: only police/accident incidents with a pursuit tag draw a
numbered path; others get unnumbered call dots. Segments over >10 min gaps or
talkgroup changes are dashed, stops implying >8 km/min are ghosted off the
path, paths >20 km get a "may be merged" badge. New optional MapView prop
pulseIncidentIds (wired by #238).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
7.5 KiB
Python
186 lines
7.5 KiB
Python
"""
|
|
server-26#159: a citywide/patched feed can be received far from its own
|
|
coverage area — "New York City - NYPD Citywide 2 Patch" was ~56km from the
|
|
receiving node, well past geocode_max_km (40km). Real, correctly-geocoded
|
|
addresses on that talkgroup were rejected by intelligence._geocode_location's
|
|
node-distance sanity check every time, so location_coords never populated for
|
|
the whole system: location_proximity correlation was permanently dead there,
|
|
and the same real event reported at two nearby addresses two minutes apart
|
|
became two separate incidents instead of one.
|
|
|
|
`trust_named_region` fixes this narrowly: the node-distance check is a proxy
|
|
for "is this plausible" that only makes sense when the node's own position is
|
|
the best guess we have at the area. It must not apply when the query already
|
|
names a different region on its own terms (operator-set area_context, or a
|
|
municipality parsed straight from the talkgroup's own name) — and it must
|
|
never touch the anchor path, whose own radius is always authoritative.
|
|
"""
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
import app.internal.intelligence as intel
|
|
from app.config import settings
|
|
|
|
|
|
def _maps_result(lat: float, lng: float, location_type: str = "ROOFTOP"):
|
|
payload = {
|
|
"status": "OK",
|
|
"results": [{
|
|
"geometry": {
|
|
"location": {"lat": lat, "lng": lng},
|
|
"location_type": location_type,
|
|
},
|
|
}],
|
|
}
|
|
|
|
class _Resp:
|
|
def raise_for_status(self): pass
|
|
def json(self): return payload
|
|
|
|
class _Client:
|
|
async def __aenter__(self): return self
|
|
async def __aexit__(self, *a): return False
|
|
async def get(self, *a, **k): return _Resp()
|
|
|
|
return patch("httpx.AsyncClient", lambda *a, **k: _Client())
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _api_key():
|
|
# intelligence.py imports settings locally per-function (`from app.config
|
|
# import settings`), which binds the same cached singleton — patching the
|
|
# module-level object here reaches it, but `intel.settings` itself does
|
|
# not exist as an attribute.
|
|
with patch.object(settings, "google_maps_api_key", "test-key"):
|
|
yield
|
|
|
|
|
|
# Node at (0, 0); result at (1, 0) is ~111km away — well past the 40km default.
|
|
NODE_LAT, NODE_LON = 0.0, 0.0
|
|
FAR_LAT, FAR_LNG = 1.0, 0.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_named_region_geocode_accepted_beyond_node_distance():
|
|
with _maps_result(FAR_LAT, FAR_LNG):
|
|
coords = await intel._geocode_location(
|
|
"1108 Jackson Avenue, New York City - NYPD Citywide 2 Patch",
|
|
node_lat=NODE_LAT, node_lon=NODE_LON,
|
|
trust_named_region=True,
|
|
)
|
|
assert coords == {"lat": FAR_LAT, "lng": FAR_LNG, "location_type": "ROOFTOP"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_local_geocode_still_rejected_beyond_node_distance_without_named_region():
|
|
"""Regression guard: a bare street name with no named region still uses
|
|
the node as its only plausibility check, exactly as before this fix."""
|
|
with _maps_result(FAR_LAT, FAR_LNG):
|
|
coords = await intel._geocode_location(
|
|
"Main Street",
|
|
node_lat=NODE_LAT, node_lon=NODE_LON,
|
|
trust_named_region=False,
|
|
)
|
|
assert coords is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_anchor_path_ignores_trust_named_region():
|
|
"""The anchor's own radius is always authoritative — trust_named_region
|
|
is only a statement about the node fallback, never a way to widen an
|
|
anchor that was itself deliberately sized to discriminate."""
|
|
anchor = {"lat": NODE_LAT, "lng": NODE_LON, "radius_km": 10.0}
|
|
with _maps_result(FAR_LAT, FAR_LNG):
|
|
coords = await intel._geocode_location(
|
|
"1108 Jackson Avenue, New York City - NYPD Citywide 2 Patch",
|
|
node_lat=NODE_LAT, node_lon=NODE_LON,
|
|
anchor=anchor, trust_named_region=True,
|
|
)
|
|
assert coords is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_named_region_geocode_within_node_distance_is_unaffected():
|
|
"""A close result is accepted the same way regardless of the flag."""
|
|
near_lat, near_lng = 0.05, 0.0 # ~5.5km from the node
|
|
with _maps_result(near_lat, near_lng):
|
|
coords = await intel._geocode_location(
|
|
"Main Street, Ossining, New York",
|
|
node_lat=NODE_LAT, node_lon=NODE_LON,
|
|
trust_named_region=True,
|
|
)
|
|
assert coords == {"lat": near_lat, "lng": near_lng, "location_type": "ROOFTOP"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_imprecise_result_still_rejected_regardless_of_trust():
|
|
"""trust_named_region relaxes the distance check only — the location_type
|
|
precision filter (server-26#37) still applies unconditionally."""
|
|
with _maps_result(FAR_LAT, FAR_LNG, location_type="APPROXIMATE"):
|
|
coords = await intel._geocode_location(
|
|
"1108 Jackson Avenue, New York City - NYPD Citywide 2 Patch",
|
|
node_lat=NODE_LAT, node_lon=NODE_LON,
|
|
trust_named_region=True,
|
|
)
|
|
assert coords is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _location_query_parts — pure query assembly, no HTTP involved
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_operator_configured_area_wins_and_is_named_region():
|
|
parts, named = intel._location_query_parts(
|
|
"High Street", {"municipality": "Yorktown", "state": "New York"},
|
|
"Tac 1", node_state="New York", node_county="Westchester",
|
|
)
|
|
assert parts == ["High Street", "Yorktown", "New York"]
|
|
assert named is True
|
|
|
|
|
|
def test_local_talkgroup_name_gets_node_state_but_not_node_county():
|
|
"""
|
|
"Ossining PD" is genuinely local to the node, so appending the node's own
|
|
state is correct. Its COUNTY is dropped even here — server-26#159's fix
|
|
applies uniformly once a municipality is derived, since there is no way
|
|
to tell "local" and "distant-but-node-adjacent" apart from the string
|
|
alone, and the county was never necessary for a bare municipality name
|
|
that already disambiguates via the state.
|
|
"""
|
|
parts, named = intel._location_query_parts(
|
|
"High Street", {}, "Ossining PD",
|
|
node_state="New York", node_county="Westchester",
|
|
)
|
|
assert parts == ["High Street", "Ossining", "New York"]
|
|
assert named is True
|
|
|
|
|
|
def test_citywide_patched_feed_does_not_get_the_nodes_county_grafted_on():
|
|
"""
|
|
server-26#159's actual production case: the talkgroup names its own
|
|
(distant) region, so the node's county (Westchester, ~56km away) must not
|
|
be appended — it would make the query self-contradictory ("...New York
|
|
City..., Westchester, New York") and risks degrading the geocode result's
|
|
precision independently of the distance check this issue also fixes.
|
|
"""
|
|
parts, named = intel._location_query_parts(
|
|
"1108 Jackson Avenue", {}, "New York City - NYPD Citywide 2 Patch",
|
|
node_state="New York", node_county="Westchester",
|
|
)
|
|
assert "Westchester" not in parts
|
|
assert parts == ["1108 Jackson Avenue", "New York City - NYPD Citywide 2 Patch", "New York"]
|
|
assert named is True
|
|
|
|
|
|
def test_uninformative_talkgroup_name_falls_back_to_node_county_and_state():
|
|
"""A tactical channel or bare code gives _municipality_from_tg nothing —
|
|
the only remaining evidence really is where the node sits, so the
|
|
original node-county-and-state fallback is preserved for this case."""
|
|
parts, named = intel._location_query_parts(
|
|
"High Street", {}, "Tac 1",
|
|
node_state="New York", node_county="Westchester",
|
|
)
|
|
assert parts == ["High Street", "Westchester", "New York"]
|
|
assert named is False
|