"""Access to the curated city set (backend/cities.json) that gets crawlable climate pages. Loaded once, lazily; regenerate the JSON with gen_cities.py.""" import json import os import paths _PATH = paths.CITIES_JSON _FLAVOR_PATH = paths.CITIES_FLAVOR_JSON _CITIES: list[dict] | None = None _BY_SLUG: dict[str, dict] | None = None _FLAVOR: dict[str, dict] | None = None _DUPES: dict[str, dict[str, int]] | None = None def _load() -> list[dict]: global _CITIES, _BY_SLUG if _CITIES is None: with open(_PATH, encoding="utf-8") as f: _CITIES = json.load(f) _BY_SLUG = {c["slug"]: c for c in _CITIES} return _CITIES def all_cities() -> list[dict]: return _load() def all_slugs() -> list[str]: return [c["slug"] for c in _load()] def get(slug: str) -> dict | None: """The city for a slug, or None (→ 404).""" _load() return _BY_SLUG.get(slug) def flavor(slug: str) -> dict | None: """A city's descriptive blurb {extract, url, title} from cities_flavor.json, or None when we have no confident match (the page renders fine without it).""" global _FLAVOR if _FLAVOR is None: try: with open(_FLAVOR_PATH, encoding="utf-8") as f: _FLAVOR = json.load(f) except (OSError, ValueError): _FLAVOR = {} return _FLAVOR.get(slug) def title_name(city: dict) -> str: """The shortest label that still names this city unambiguously — for page titles, where display_name()'s region + country spend the width Google gives us before it ever reaches the payload ("… in July"). 'Seattle' for the 968 cities whose name is unique; 'London, GB' when the name recurs in another country; 'Columbus, Ohio' when it recurs *within* one country, where the country code would collide too and hand two different pages the same title. """ name = city["name"] dupes = _dupe_names().get(name) if not dupes: return name if dupes.get(city.get("country_code")) == 1: return f"{name}, {city['country_code']}" return f"{name}, {city['admin1']}" if city.get("admin1") else name def _dupe_names() -> dict[str, dict[str, int]]: """{name: {country_code: how many cities share both}} for duplicated names only. Built once alongside the city list.""" global _DUPES if _DUPES is None: counts: dict[str, dict[str, int]] = {} for c in _load(): counts.setdefault(c["name"], {}) cc = c.get("country_code") counts[c["name"]][cc] = counts[c["name"]].get(cc, 0) + 1 _DUPES = {n: by_cc for n, by_cc in counts.items() if sum(by_cc.values()) > 1} return _DUPES def display_name(city: dict) -> str: """Human label: 'Seattle, Washington, United States' (drops repeated admin1).""" parts = [city["name"]] if city.get("admin1") and city["admin1"] != city["name"]: parts.append(city["admin1"]) if city.get("country"): parts.append(city["country"]) return ", ".join(parts) def by_country() -> dict[str, list[dict]]: """Cities grouped by country, both the countries and the cities within each ordered alphabetically — for the /climate hub's crawlable link graph + search.""" groups: dict[str, list[dict]] = {} for c in _load(): key = c.get("country") or c.get("country_code") or "Other" groups.setdefault(key, []).append(c) for v in groups.values(): v.sort(key=lambda x: x["name"].lower()) return dict(sorted(groups.items(), key=lambda kv: kv[0].lower()))