"""City search (offline, geonamescache) + historical timezone resolution.""" 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 _index(): global _CITY_INDEX if _CITY_INDEX is None: rows = [] 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["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