Files
mohamad d4ec85add4 feat: add configurable people maps
Add independent map profiles, offline city lookup, bundled world artwork
and collision-free portrait placement with optional person details.

Deploy with updated requirements, migrations 0029-0031 and collectstatic.
2026-11-03 04:41:47 +08:00

36 lines
1.5 KiB
Python

"""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']