"""Offline city resolution. No requests or personal data leave the server.""" from functools import lru_cache import unicodedata from django.core.exceptions import ValidationError def normalize(value): return ''.join(c for c in unicodedata.normalize('NFKD', value.casefold().strip()) if not unicodedata.combining(c)) @lru_cache(maxsize=1) def gazetteer(): import geonamescache return geonamescache.GeonamesCache() def resolve_city(country, city): data = gazetteer() country_key = normalize(country) codes = [c['iso'] for c in data.get_countries().values() if country_key in (normalize(c['name']), c['iso'].lower(), c['iso3'].lower())] if not codes: raise ValidationError({'country': 'Enter a country name or ISO code, e.g. Canada or CA.'}) city_key = normalize(city) country_cities = [c for c in data.get_cities().values() if c['countrycode'] in codes] # Prefer the city's canonical name over another town's historical aliases. # E.g. Tehran is also listed as an alias of Tiran in GeoNames. matches = [c for c in country_cities if normalize(c['name']) == city_key] if not matches: matches = [c for c in country_cities if city_key in { normalize(alias) for alias in c.get('alternatenames', []) }] if len(matches) != 1: raise ValidationError({'city': 'City not found or ambiguous. Use coordinates for small towns or cities with the same name.'}) return matches[0]['latitude'], matches[0]['longitude']