"""City search (offline, geonamescache + custom_cities.json) + historical timezone resolution.""" import json import os from datetime import datetime from zoneinfo import ZoneInfo import geonamescache from timezonefinder import TimezoneFinder _gc = geonamescache.GeonamesCache() _tf = TimezoneFinder() _countries = {c["iso"]: c["name"] for c in _gc.get_countries().values()} _CITY_INDEX = None def _custom_cities(): """Supplemental cities missing from the geonames 15k+ dataset (custom_cities.json).""" path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "custom_cities.json") if not os.path.exists(path): return [] with open(path) as f: rows = [] for c in json.load(f): rows.append({ "name": c["name"], "search": c["name"].lower(), "alt": [a.lower() for a in c.get("alt", [])], "country": c["country"], "cc": c["cc"], "admin": c.get("admin", ""), "lat": float(c["lat"]), "lon": float(c["lon"]), "pop": int(c.get("pop", 0)), "tz": c["tz"], }) return rows def _index(): global _CITY_INDEX if _CITY_INDEX is None: rows = _custom_cities() for c in _gc.get_cities().values(): rows.append({ "name": c["name"], "search": c["name"].lower(), "alt": [a.lower() for a in c.get("alternatenames", [])[:20]], "country": _countries.get(c["countrycode"], c["countrycode"]), "cc": c["countrycode"], "admin": c.get("admin1code", ""), "lat": c["latitude"], "lon": c["longitude"], "pop": c["population"], "tz": c["timezone"], }) rows.sort(key=lambda r: -r["pop"]) _CITY_INDEX = rows return _CITY_INDEX def search_cities(q, limit=12): q = q.strip().lower() if len(q) < 2: return [] starts, contains = [], [] for r in _index(): if r["search"].startswith(q): starts.append(r) elif q in r["search"] or any(a.startswith(q) for a in r["alt"]): contains.append(r) if len(starts) >= limit: break rows = (starts + contains)[:limit] return [{"name": r["name"], "country": r["country"], "cc": r["cc"], "admin": r["admin"], "lat": r["lat"], "lon": r["lon"], "tz": r["tz"], "label": (f'{r["name"]}, {r["admin"]}, {r["country"]}' if r["admin"] and not r["admin"].isdigit() else f'{r["name"]}, {r["country"]}')} for r in rows] def resolve_offset(lat, lon, year, month, day, hour, minute, tz_hint=None): """Return (utc_offset_hours, tz_name) for the LOCAL wall-clock birth time. Uses the IANA tz database, which carries historical offsets and DST rules — the same thing the 90s programs shipped as their 'atlas'. """ tzname = tz_hint or _tf.timezone_at(lat=lat, lng=lon) if not tzname: tzname = _tf.closest_timezone_at(lat=lat, lng=lon) if hasattr(_tf, "closest_timezone_at") else "UTC" tz = ZoneInfo(tzname) local = datetime(year, month, day, hour, minute, tzinfo=tz) off = local.utcoffset().total_seconds() / 3600.0 return off, tzname