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.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user