A unit call-sign is not a place
"Post 1-2" reached the geocoder, resolved against its talkgroup anchor and produced a confident pin in the right town for an event with no known location — while sitting in the same incident's `units` list the whole time. A plausible wrong pin is worse than no pin: nothing downstream can tell it is wrong. Extraction returns `location` and `units` from one pass, so a string in both is a misclassification, not two facts. Drop it before the geocoder sees it. Closes server-26#52. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
964343c819
commit
cc038e6326
@@ -282,6 +282,27 @@ def clean_location(value) -> Optional[str]:
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def location_is_unit(location, units) -> bool:
|
||||||
|
"""
|
||||||
|
True when a location label is really one of the incident's own unit
|
||||||
|
call-signs.
|
||||||
|
|
||||||
|
Extraction returns `location` and `units` from the same pass, so a string
|
||||||
|
appearing in both is a misclassification, not two facts. "Post 1-2" reached
|
||||||
|
the geocoder that way, resolved against its talkgroup anchor, and produced a
|
||||||
|
confident pin in the right town for an event that has no known location at
|
||||||
|
all — worse than no pin, because nothing downstream can tell it is wrong.
|
||||||
|
See server-26#52.
|
||||||
|
|
||||||
|
Public because `intelligence.py` applies it at extraction time, alongside
|
||||||
|
`clean_location`, so the string never reaches the geocoder.
|
||||||
|
"""
|
||||||
|
key = _place_key(location)
|
||||||
|
if not key:
|
||||||
|
return False
|
||||||
|
return any(key == _place_key(u) for u in (units or []))
|
||||||
|
|
||||||
|
|
||||||
def _place_key(value) -> str:
|
def _place_key(value) -> str:
|
||||||
"""Case- and punctuation-blind key for comparing two location labels."""
|
"""Case- and punctuation-blind key for comparing two location labels."""
|
||||||
return re.sub(r"[^a-z0-9]+", " ", str(value or "").lower()).strip()
|
return re.sub(r"[^a-z0-9]+", " ", str(value or "").lower()).strip()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from app.internal import area_context
|
|||||||
# Location validity is defined once, by the module that owns the incident's
|
# Location validity is defined once, by the module that owns the incident's
|
||||||
# location/pin invariant. incident_correlator does not import this module, so
|
# location/pin invariant. incident_correlator does not import this module, so
|
||||||
# this is not a cycle.
|
# this is not a cycle.
|
||||||
from app.internal.incident_correlator import clean_location
|
from app.internal.incident_correlator import clean_location, location_is_unit
|
||||||
|
|
||||||
_PROMPT_TEMPLATE = """You are analyzing a P25 public safety radio recording. The audio was transcribed by Whisper through a digital radio vocoder, which introduces errors. Each numbered transmission is a separate PTT press from a different radio.
|
_PROMPT_TEMPLATE = """You are analyzing a P25 public safety radio recording. The audio was transcribed by Whisper through a digital radio vocoder, which introduces errors. Each numbered transmission is a separate PTT press from a different radio.
|
||||||
|
|
||||||
@@ -252,6 +252,18 @@ async def extract_scenes(
|
|||||||
location: Optional[str] = clean_location(scene.get("location"))
|
location: Optional[str] = clean_location(scene.get("location"))
|
||||||
vehicles: list[str] = scene.get("vehicles") or []
|
vehicles: list[str] = scene.get("vehicles") or []
|
||||||
units: list[str] = scene.get("units") or []
|
units: list[str] = scene.get("units") or []
|
||||||
|
# A "location" that is also one of this scene's own units is a unit
|
||||||
|
# call-sign, not a place. Both lists come from the same extraction pass,
|
||||||
|
# so the disagreement is free to detect and the string must be dropped
|
||||||
|
# before it reaches the geocoder — anchored place verification will
|
||||||
|
# otherwise resolve "Post 1-2" to a confident, plausible, wrong pin in
|
||||||
|
# the right town. See server-26#52.
|
||||||
|
if location and location_is_unit(location, units):
|
||||||
|
logger.info(
|
||||||
|
f"Intelligence: dropping location {location!r} — it is one of "
|
||||||
|
f"this scene's units, not a place"
|
||||||
|
)
|
||||||
|
location = None
|
||||||
cleared_units: list[str] = scene.get("cleared_units") or []
|
cleared_units: list[str] = scene.get("cleared_units") or []
|
||||||
# Every call carries a severity — it is the signal the correlator uses to
|
# Every call carries a severity — it is the signal the correlator uses to
|
||||||
# decide whether a call is incident-worthy at all, so it must never be
|
# decide whether a call is incident-worthy at all, so it must never be
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from unittest.mock import AsyncMock, patch
|
|||||||
|
|
||||||
from app.internal.incident_correlator import (
|
from app.internal.incident_correlator import (
|
||||||
_build_context, _create_incident, _update_incident,
|
_build_context, _create_incident, _update_incident,
|
||||||
_resolve_location_pair, _verified_pin, clean_location,
|
_resolve_location_pair, _verified_pin, clean_location, location_is_unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
NOW = datetime(2026, 8, 20, 7, 25, 0, tzinfo=timezone.utc)
|
NOW = datetime(2026, 8, 20, 7, 25, 0, tzinfo=timezone.utc)
|
||||||
@@ -340,3 +340,35 @@ async def test_a_legacy_incidents_title_is_not_claimed_by_the_next_call():
|
|||||||
incident_type="police", severity="routine")
|
incident_type="police", severity="routine")
|
||||||
assert "title" not in updates
|
assert "title" not in updates
|
||||||
assert updates["location"] == "80 Grasslands Road"
|
assert updates["location"] == "80 Grasslands Road"
|
||||||
|
|
||||||
|
|
||||||
|
# ── server-26#52: a unit call-sign must never become a map pin ────────────────
|
||||||
|
#
|
||||||
|
# "Post 1-2" passed clean_location (it has a word in it), geocoded against the
|
||||||
|
# Ossining anchor and produced a confident pin in the right town for an event
|
||||||
|
# with no known location. It was in the same incident's `units` all along.
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("location,units", [
|
||||||
|
("Post 1-2", ["1-2", "Lincoln", "Post 1-2"]), # the dump's actual incident
|
||||||
|
("post 1-2", ["Post 1-2"]), # case-blind
|
||||||
|
("Post 1-2.", ["Post 1-2"]), # punctuation-blind
|
||||||
|
("Engine 4", ["Engine 4", "Ladder 1"]),
|
||||||
|
])
|
||||||
|
def test_location_matching_a_unit_is_rejected(location, units):
|
||||||
|
assert location_is_unit(location, units) is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("location,units", [
|
||||||
|
("Water Street", ["1-2", "Post 1-2"]), # a real place, same incident
|
||||||
|
("South High", []), # no units extracted
|
||||||
|
("Riverdale Station", ["Lincoln"]),
|
||||||
|
("", ["Post 1-2"]), # nothing to compare
|
||||||
|
(None, ["Post 1-2"]),
|
||||||
|
])
|
||||||
|
def test_real_places_survive_the_unit_check(location, units):
|
||||||
|
assert location_is_unit(location, units) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_unit_check_does_not_match_on_substrings():
|
||||||
|
"""`1-2` is a unit; "1-2 Main Street" is an address that contains it."""
|
||||||
|
assert location_is_unit("1-2 Main Street", ["1-2"]) is False
|
||||||
|
|||||||
Reference in New Issue
Block a user