Compare commits
2 Commits
1c2c67d854
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16da230813 | ||
|
|
0568e53ee9 |
@@ -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",
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
traefik:
|
traefik:
|
||||||
external: true
|
external: true
|
||||||
|
name: traefik-network
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user