Compare commits
2 Commits
1c2c67d854
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16da230813 | ||
|
|
0568e53ee9 |
@@ -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",
|
||||
|
||||
@@ -14,3 +14,4 @@ services:
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
name: traefik-network
|
||||
|
||||
@@ -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