Implement story 1.4: location resolution ICAO and address

Adds location.resolve() with ICAO CSV lookup and Nominatim geocoding,
POST /find-location portal route with inline confirmation/error display,
and full test coverage with mocked network calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Edholm
2026-04-22 22:46:31 -04:00
parent d388cca478
commit 6231e3157e
5 changed files with 214 additions and 38 deletions
+50 -1
View File
@@ -1 +1,50 @@
# stub
import csv
import importlib.resources
import logging
import requests
log = logging.getLogger(__name__)
NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"
_USER_AGENT = "planemapper/0.1 (https://github.com/football2801/planeMapper)"
def _lookup_icao(code: str) -> tuple[float, float, str] | None:
traversable = importlib.resources.files("planemapper.data").joinpath("airports.csv")
with traversable.open("r", encoding="utf-8") as fh:
reader = csv.DictReader(fh)
for row in reader:
if row["ident"] == code:
return float(row["latitude_deg"]), float(row["longitude_deg"]), row["name"]
return None
def _geocode(query: str) -> tuple[float, float, str] | None:
resp = requests.get(
NOMINATIM_URL,
params={"q": query, "format": "json", "limit": 1},
headers={"User-Agent": _USER_AGENT},
timeout=10,
)
resp.raise_for_status()
results = resp.json()
if not results:
return None
r = results[0]
return float(r["lat"]), float(r["lon"]), r["display_name"]
def resolve(query: str) -> tuple[float, float, str]:
query = query.strip().upper()
if len(query) == 4 and query.isalpha():
result = _lookup_icao(query)
if result is None:
raise ValueError("ICAO code not found — try an address instead")
log.info("ICAO %s resolved to %s", query, result[2])
return result
result = _geocode(query)
if result is None:
raise ValueError("Location not found — try a different search term")
log.info("Address '%s' resolved to %s", query, result[2])
return result