147 lines
4.2 KiB
Python
147 lines
4.2 KiB
Python
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()
|
|
response = await client.get(f"{NOMINATIM_BASE_URL}{path}", params=params, headers=headers, timeout=12.0)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
|
|
async def search_places(query: str, limit: int = 8) -> list[dict[str, Any]]:
|
|
query = query.strip()
|
|
if len(query) < 2:
|
|
return []
|
|
if query.isdigit() and len(query) < 4:
|
|
return []
|
|
|
|
qhash = _query_hash(f"search:{query}:{limit}")
|
|
cached = await get_cached_json("geocode_cache", {"query_hash": qhash}, CACHE_TTL_GEOCODE)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
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",
|
|
{"query_hash": qhash, "query_text": query},
|
|
results,
|
|
)
|
|
return results
|
|
|
|
|
|
async def reverse_geocode(lat: float, lon: float) -> dict[str, Any]:
|
|
qhash = _query_hash(f"reverse:{lat:.4f}:{lon:.4f}")
|
|
cached = await get_cached_json("geocode_cache", {"query_hash": qhash}, CACHE_TTL_GEOCODE)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
raw = await _nominatim_get(
|
|
"/reverse",
|
|
{"lat": lat, "lon": lon, "format": "jsonv2", "addressdetails": 1, "zoom": 10},
|
|
)
|
|
address = raw.get("address") or {}
|
|
payload = {
|
|
"label": raw.get("display_name"),
|
|
"place": address.get("city")
|
|
or address.get("town")
|
|
or address.get("village")
|
|
or address.get("municipality")
|
|
or address.get("county"),
|
|
"county": address.get("county"),
|
|
"state": address.get("state"),
|
|
"postcode": address.get("postcode"),
|
|
"admin_names": [
|
|
value
|
|
for value in [
|
|
address.get("city"),
|
|
address.get("town"),
|
|
address.get("village"),
|
|
address.get("municipality"),
|
|
address.get("county"),
|
|
address.get("state"),
|
|
]
|
|
if value
|
|
],
|
|
}
|
|
await set_cached_json(
|
|
"geocode_cache",
|
|
{"query_hash": qhash, "query_text": f"{lat},{lon}"},
|
|
payload,
|
|
)
|
|
return payload
|