// Geocode the 16 World Cup 2026 venues once (Open-Meteo geocoding, keyless) // → public/data/venues.json with lat/lon/elevation/timezone per fixture-venue // string. Elevation matters here: Mexico City sits at ~2,240m. import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const OUT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'public', 'data'); /** fixture venue string → geocoding query + expected country (disambiguator) */ const VENUES: Record = { 'Atlanta': { q: 'Atlanta', cc: 'US' }, 'Boston (Foxborough)': { q: 'Foxborough', cc: 'US' }, 'Dallas (Arlington)': { q: 'Arlington', cc: 'US' }, 'Guadalajara (Zapopan)': { q: 'Zapopan', cc: 'MX' }, 'Houston': { q: 'Houston', cc: 'US' }, 'Kansas City': { q: 'Kansas City', cc: 'US' }, 'Los Angeles (Inglewood)': { q: 'Inglewood', cc: 'US' }, 'Mexico City': { q: 'Mexico City', cc: 'MX' }, 'Miami (Miami Gardens)': { q: 'Miami Gardens', cc: 'US' }, 'Monterrey (Guadalupe)': { q: 'Guadalupe', cc: 'MX' }, 'New York/New Jersey (East Rutherford)': { q: 'East Rutherford', cc: 'US' }, 'Philadelphia': { q: 'Philadelphia', cc: 'US' }, 'San Francisco Bay Area (Santa Clara)': { q: 'Santa Clara', cc: 'US' }, 'Seattle': { q: 'Seattle', cc: 'US' }, 'Toronto': { q: 'Toronto', cc: 'CA' }, 'Vancouver': { q: 'Vancouver', cc: 'CA' }, }; /** Geocoder misses (e.g. "Vancouver" resolving to a Vancouver-Island point): * pin the known city coordinates explicitly. */ const OVERRIDES: Record = { Vancouver: { lat: 49.2827, lon: -123.1207, elevation: 70, tz: 'America/Vancouver' }, }; interface GeoResult { results?: { latitude: number; longitude: number; elevation?: number; timezone?: string; name: string; country_code?: string }[]; } async function main(): Promise { const out: Record = {}; for (const [venue, { q, cc }] of Object.entries(VENUES)) { const override = OVERRIDES[venue]; if (override) { out[venue] = override; console.log(venue, '→ (override)', override); continue; } const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(q)}&count=10&language=en&format=json`; const res = await fetch(url); const d = (await res.json()) as GeoResult & { results?: { population?: number }[] }; const candidates = (d.results ?? []).filter((x) => x.country_code === cc); const r = candidates.sort((a, b) => ((b as { population?: number }).population ?? 0) - ((a as { population?: number }).population ?? 0))[0]; if (!r) throw new Error(`no geocoding result for ${venue} (${q}, ${cc})`); out[venue] = { lat: +r.latitude.toFixed(4), lon: +r.longitude.toFixed(4), elevation: Math.round(r.elevation ?? 0), tz: r.timezone ?? 'UTC', }; console.log(venue, '→', out[venue]); await new Promise((resolve) => setTimeout(resolve, 250)); } mkdirSync(OUT_DIR, { recursive: true }); writeFileSync(join(OUT_DIR, 'venues.json'), JSON.stringify({ generatedAt: new Date().toISOString(), venues: out }, null, 1)); console.log(`venues → public/data/venues.json (${Object.keys(out).length})`); } void main();