Enhance geocoding service and frontend search functionality. Refactor geocoding logic to improve postal code handling and response parsing. Update frontend searchPlaces function to include postal code in results display and adjust input validation. Modify CSS for improved search results styling.
All checks were successful
CI / api-check (push) Successful in 50s
CI / frontend-check (push) Successful in 7s

This commit is contained in:
TheOnlyMace
2026-06-28 23:07:29 +02:00
parent 0568e53ee9
commit 16da230813
3 changed files with 74 additions and 39 deletions

View File

@@ -1,17 +1,78 @@
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import re
from typing import Any from typing import Any
from app.config import CACHE_TTL_GEOCODE, NOMINATIM_BASE_URL, NOMINATIM_USER_AGENT from app.config import CACHE_TTL_GEOCODE, NOMINATIM_BASE_URL, NOMINATIM_USER_AGENT
from app.http_client import get_client from app.http_client import get_client
from app.database import get_cached_json, set_cached_json 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: def _query_hash(query: str) -> str:
return hashlib.sha256(query.strip().lower().encode("utf-8")).hexdigest() 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: async def _nominatim_get(path: str, params: dict[str, Any]) -> Any:
headers = {"User-Agent": NOMINATIM_USER_AGENT, "Accept-Language": "de"} headers = {"User-Agent": NOMINATIM_USER_AGENT, "Accept-Language": "de"}
client = get_client() 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]]: 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 [] return []
qhash = _query_hash(f"search:{query}:{limit}") 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: if cached is not None:
return cached return cached
raw = await _nominatim_get( raw = await _nominatim_get("/search", _build_search_params(query, limit))
"/search", results = [_parse_nominatim_item(item) for item in raw]
{
"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"),
}
)
await set_cached_json( await set_cached_json(
"geocode_cache", "geocode_cache",

View File

@@ -758,7 +758,7 @@ function toggleWarningsOnMap() {
} }
async function searchPlaces(query) { async function searchPlaces(query) {
if (query.length < 2) { if (query.length < 2 || (/^\d+$/.test(query) && query.length < 4)) {
el("searchResults").classList.add("hidden"); el("searchResults").classList.add("hidden");
return; return;
} }
@@ -767,7 +767,7 @@ async function searchPlaces(query) {
const results = data.results || []; const results = data.results || [];
el("searchResults").innerHTML = results.length ? results.map((r, i) => ` el("searchResults").innerHTML = results.length ? results.map((r, i) => `
<li role="option" data-index="${i}"> <li role="option" data-index="${i}">
<strong>${r.place}</strong> <strong>${r.place}${r.postcode ? ` (${r.postcode})` : ""}</strong>
<span>${r.label}</span> <span>${r.label}</span>
</li> </li>
`).join("") : `<li class="muted">Keine Treffer</li>`; `).join("") : `<li class="muted">Keine Treffer</li>`;
@@ -776,6 +776,7 @@ async function searchPlaces(query) {
item.addEventListener("click", () => { item.addEventListener("click", () => {
const r = results[Number(item.dataset.index)]; const r = results[Number(item.dataset.index)];
setLocation({ place: r.place, lat: r.lat, lon: r.lon }); setLocation({ place: r.place, lat: r.lat, lon: r.lon });
el("placeSearch").value = r.postcode || r.place;
el("searchResults").classList.add("hidden"); el("searchResults").classList.add("hidden");
loadAll(); loadAll();
}); });

View File

@@ -249,11 +249,13 @@ html[data-theme="light"] textarea { background: rgba(255,255,255,0.92); }
grid-template-columns: 2fr 1.2fr 1fr 1fr auto; grid-template-columns: 2fr 1.2fr 1fr 1fr auto;
gap: 12px; gap: 12px;
align-items: end; align-items: end;
position: relative;
z-index: 30;
} }
.search-wrap { position: relative; } .search-wrap { position: relative; z-index: 2; }
.search-results { .search-results {
position: absolute; position: absolute;
z-index: 50; z-index: 100;
left: 0; right: 0; left: 0; right: 0;
top: calc(100% + 6px); top: calc(100% + 6px);
list-style: none; list-style: none;