Selbst gehostete PWA, die Visitenkarten von einem Foto freistellt, ausliest und als Kontakt bereitstellt. - Stapelscan: OpenCV findet die Kartenrechtecke über mehrere Binärmasken, entzerrt sie perspektivisch und schneidet sie einzeln aus. Ohne Fund gilt das ganze Foto als eine Karte. - Extraktion: ein Aufruf je Zuschnitt an das Vision-Modell mit JSON-Schema. Kein vorgeschaltetes OCR - das würde Layout und Schriftgrößen wegwerfen, aus denen die Feldzuordnung entsteht. - Metadaten: Aufnahmezeit und GPS aus den EXIF-Daten des Fotos, Ortsname über Nominatim, Browserstandort nur als Rückfallebene. - Übersicht mit Volltextsuche und Filtern, Detailansicht mit Korrekturmaske. - Notizfeld je Karte, Erinnerungen per Mail inklusive Nachholen verpasster Termine nach einem Neustart. - vCard 3.0 einzeln und als Sammeldatei, Karte gilt danach als exportiert. - Anmeldung über ein Passwort, Sitzung als signiertes Cookie. - Deployment per Tag-Push nach den Konventionen in DEPLOY.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
53 lines
1.8 KiB
Python
53 lines
1.8 KiB
Python
"""Reverse-Geocoding ueber Nominatim, Ergebnisse werden dauerhaft gecacht."""
|
|
import json
|
|
import urllib.parse
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
|
|
from . import config, db
|
|
|
|
|
|
def _label(address: dict) -> str:
|
|
city = (
|
|
address.get("city")
|
|
or address.get("town")
|
|
or address.get("village")
|
|
or address.get("municipality")
|
|
or address.get("county")
|
|
)
|
|
detail = address.get("suburb") or address.get("neighbourhood") or address.get("road")
|
|
parts = [p for p in (city, detail) if p]
|
|
if not parts:
|
|
return address.get("state") or address.get("country") or ""
|
|
return ", ".join(parts[:2])
|
|
|
|
|
|
def reverse(lat: float | None, lon: float | None) -> str | None:
|
|
if lat is None or lon is None or not config.REVERSE_GEOCODE:
|
|
return None
|
|
|
|
# Auf ~100 m runden: benachbarte Scans teilen sich denselben Cache-Eintrag.
|
|
key = f"{lat:.3f},{lon:.3f}"
|
|
cached = db.query_one("SELECT place FROM geocache WHERE key = ?", (key,))
|
|
if cached is not None:
|
|
return cached["place"]
|
|
|
|
url = config.NOMINATIM_URL + "?" + urllib.parse.urlencode(
|
|
{"lat": lat, "lon": lon, "format": "jsonv2", "zoom": "16", "accept-language": "de"}
|
|
)
|
|
place = None
|
|
try:
|
|
request = urllib.request.Request(url, headers={"User-Agent": config.USER_AGENT})
|
|
with urllib.request.urlopen(request, timeout=6) as response:
|
|
data = json.loads(response.read().decode("utf-8"))
|
|
place = _label(data.get("address") or {}) or data.get("display_name")
|
|
except Exception:
|
|
# Ohne Ortsnamen bleiben die Koordinaten - kein Grund, den Scan abzubrechen.
|
|
return None
|
|
|
|
db.execute(
|
|
"INSERT OR REPLACE INTO geocache (key, place, fetched_at) VALUES (?, ?, ?)",
|
|
(key, place, datetime.now(timezone.utc).isoformat()),
|
|
)
|
|
return place
|