Compare commits

...

2 Commits

Author SHA1 Message Date
TheOnlyMace
16da230813 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
2026-06-28 23:07:29 +02:00
TheOnlyMace
0568e53ee9 Update docker-compose.traefik.yml to specify the name of the external Traefik network
All checks were successful
CI / api-check (push) Successful in 11s
CI / frontend-check (push) Successful in 6s
2026-06-19 22:14:34 +02:00
4 changed files with 75 additions and 39 deletions

View File

@@ -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",

View File

@@ -14,3 +14,4 @@ services:
networks:
traefik:
external: true
name: traefik-network

View File

@@ -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) => `
<li role="option" data-index="${i}">
<strong>${r.place}</strong>
<strong>${r.place}${r.postcode ? ` (${r.postcode})` : ""}</strong>
<span>${r.label}</span>
</li>
`).join("") : `<li class="muted">Keine Treffer</li>`;
@@ -776,6 +776,7 @@ async function searchPlaces(query) {
item.addEventListener("click", () => {
const r = results[Number(item.dataset.index)];
setLocation({ place: r.place, lat: r.lat, lon: r.lon });
el("placeSearch").value = r.postcode || r.place;
el("searchResults").classList.add("hidden");
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;
gap: 12px;
align-items: end;
position: relative;
z-index: 30;
}
.search-wrap { position: relative; }
.search-wrap { position: relative; z-index: 2; }
.search-results {
position: absolute;
z-index: 50;
z-index: 100;
left: 0; right: 0;
top: calc(100% + 6px);
list-style: none;