diff --git a/api/app/services/geocoding.py b/api/app/services/geocoding.py index 53b212f..ff6679d 100644 --- a/api/app/services/geocoding.py +++ b/api/app/services/geocoding.py @@ -1,17 +1,78 @@ from __future__ import annotations import hashlib +import re from typing import Any from app.config import CACHE_TTL_GEOCODE, NOMINATIM_BASE_URL, NOMINATIM_USER_AGENT from app.http_client import get_client from app.database import get_cached_json, set_cached_json +_POSTAL_ONLY = re.compile(r"^\d{4,5}$") +_POSTAL_CITY = re.compile(r"^(\d{4,5})\s+(.+)$") + def _query_hash(query: str) -> str: return hashlib.sha256(query.strip().lower().encode("utf-8")).hexdigest() +def _parse_nominatim_item(item: dict[str, Any]) -> dict[str, Any]: + address = item.get("address") or {} + label = item.get("display_name", "") + place = ( + address.get("city") + or address.get("town") + or address.get("village") + or address.get("municipality") + or address.get("county") + or label.split(",")[0] + ) + return { + "label": label, + "place": place, + "lat": float(item["lat"]), + "lon": float(item["lon"]), + "type": item.get("type"), + "postcode": address.get("postcode"), + "county": address.get("county"), + "state": address.get("state"), + } + + +def _country_codes_for_postal(postal: str) -> str: + if len(postal) == 5: + return "de" + return "at,ch" + + +def _build_search_params(query: str, limit: int) -> dict[str, Any]: + base = { + "format": "jsonv2", + "addressdetails": 1, + "limit": limit, + } + city_match = _POSTAL_CITY.match(query) + if city_match: + postal, city = city_match.groups() + return { + **base, + "postalcode": postal, + "city": city.strip(), + "countrycodes": _country_codes_for_postal(postal), + } + if _POSTAL_ONLY.match(query): + return { + **base, + "postalcode": query, + "countrycodes": _country_codes_for_postal(query), + } + return { + **base, + "q": query, + "countrycodes": "de,at,ch", + } + + async def _nominatim_get(path: str, params: dict[str, Any]) -> Any: headers = {"User-Agent": NOMINATIM_USER_AGENT, "Accept-Language": "de"} client = get_client() @@ -21,7 +82,10 @@ async def _nominatim_get(path: str, params: dict[str, Any]) -> Any: async def search_places(query: str, limit: int = 8) -> list[dict[str, Any]]: - if len(query.strip()) < 2: + query = query.strip() + if len(query) < 2: + return [] + if query.isdigit() and len(query) < 4: return [] qhash = _query_hash(f"search:{query}:{limit}") @@ -29,40 +93,8 @@ async def search_places(query: str, limit: int = 8) -> list[dict[str, Any]]: if cached is not None: return cached - raw = await _nominatim_get( - "/search", - { - "q": query, - "format": "jsonv2", - "addressdetails": 1, - "limit": limit, - "countrycodes": "de,at,ch", - }, - ) - results = [] - for item in raw: - address = item.get("address") or {} - label = item.get("display_name", "") - place = ( - address.get("city") - or address.get("town") - or address.get("village") - or address.get("municipality") - or address.get("county") - or label.split(",")[0] - ) - results.append( - { - "label": label, - "place": place, - "lat": float(item["lat"]), - "lon": float(item["lon"]), - "type": item.get("type"), - "postcode": address.get("postcode"), - "county": address.get("county"), - "state": address.get("state"), - } - ) + raw = await _nominatim_get("/search", _build_search_params(query, limit)) + results = [_parse_nominatim_item(item) for item in raw] await set_cached_json( "geocode_cache", diff --git a/frontend/app.js b/frontend/app.js index dc2fd8e..ac0ca8b 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -758,7 +758,7 @@ function toggleWarningsOnMap() { } async function searchPlaces(query) { - if (query.length < 2) { + if (query.length < 2 || (/^\d+$/.test(query) && query.length < 4)) { el("searchResults").classList.add("hidden"); return; } @@ -767,7 +767,7 @@ async function searchPlaces(query) { const results = data.results || []; el("searchResults").innerHTML = results.length ? results.map((r, i) => `