Parse field-report place descriptions into candidate anchors and bounded regions, then hold ambiguous directions and distances for review.
Project: ground field-report locations without false map pins
Create the field-report corpus
Use synthetic reports such as “within 3 kilometres of Cedar Depot” and “near Cedar Depot.” Add two facilities with the same name in different districts, a renamed clinic, a border case and a direction relative to a route. Annotate source spans, anchor ID, relation, numeric value, unit, geographic type and whether a map filter is allowed. Keep the report text and the gazetteer snapshot. Frame extraction provides the first reviewable representation.
Implement staged resolution
Identify place spans, retrieve authorized gazetteer candidates, resolve only with independent context and parse the relation. Translate an explicit distance to a typed value under a unit policy. Do not accept “near” as a radius and do not turn “north of” into a single point. The code below checks the final release state; a spatial service must still perform the actual distance test with a declared coordinate system. Spatial scope sets that boundary.
Review answer and display behavior
For every report, compare the chosen anchor and represented extent against the annotated fixture. Inspect map output for false precision: an unresolved report should show a text description and review marker, not a precise pin. Separate extraction error, wrong gazetteer match, wrong relation and geometry error. Test that users cannot query a restricted facility through a place alias.
Set a release gate
Report candidate recall, anchor-link precision, relation accuracy, bounded-region correctness and the percentage correctly held for clarification. Add human review of a sample of accepted regions. A location workflow can release text indexing before automated spatial filtering if the latter has unresolved ambiguity. Record every gazetteer update that changes an existing report’s resolved anchor so map results can be recomputed deliberately.
Implementation
def release_location_record(report, permitted_place_ids):
if report.get("relation_state") != "bounded":
return {"state": "text-only", "reason": "unresolved-relation"}
anchor_id = report.get("anchor_id")
if anchor_id not in permitted_place_ids:
return {"state": "hold", "reason": "anchor-scope"}
if not report.get("gazetteer_version") or not report.get("source_span"):
return {"state": "hold", "reason": "missing-provenance"}
return {"state": "region-ready", "anchor_id": anchor_id,
"gazetteer_version": report["gazetteer_version"]}
report = {"relation_state": "bounded", "anchor_id": "depot-47",
"gazetteer_version": "gazetteer-r7", "source_span": [19, 30]}
assert release_location_record(report, {"depot-47"})["state"] == "region-ready"
assert release_location_record({**report, "relation_state": "vague"},
{"depot-47"})["state"] == "text-only"
Performance and operating cost
The final release check is expected O(1) time and space with a hash-backed place set. Candidate retrieval, linguistic parsing and spatial filtering dominate system cost. “Region-ready” only authorizes the next spatial computation; it must not be rendered as a verified point location.
Common Mistakes
- Drawing a pin at the anchor rather than representing the described area.
- Guessing a radius for a vague relation.
- Losing the gazetteer version needed to revisit old reports.
- Scoring only toponym detection while ignoring relation errors.
