Files
cup26/scripts/buildVenues.ts
T
NilsBriggen 517a86233a v2 Phase D: capture-first ingestion — FotMob, FIFA, weather, scorer props
Four new feeds, all verified against real tournament matches:
- FotMob (unofficial, works from the VPS again): per-shot xG/xGOT with
  pitch coords, team xG, player ratings, POTM, momentum, attacking
  zones — trimmed payloads (~87KB/match) on a gentle 90s-live/30m-idle
  cadence. Capture-first: this source could lock again any day.
- FIFA official v3: confirmed lineups with player IDs, typed XY event
  timelines (80 events for the opener), attendance + named referees,
  tactics — id-mapped via the calendar, hot while live.
- Open-Meteo: kickoff-hour weather per venue from a build-time geocoded
  venues.json (with elevation — Azteca 2,240m); fetched in the enrich
  loop inside the 48h horizon.
- DraftKings scorer props (first/last/anytime goalscorer per athlete)
  via ESPN's core propBets, insert-if-changed line history, athlete
  names resolved through a 30-day cache.

Also fixes a real resilience gap found along the way: fixture results
now persist in SQLite and restore at boot — previously a restart after
ESPN's scoreboard window moved on would silently forget a match day
(standings, model and scoreboard would all regress).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:19:10 +02:00

70 lines
3.3 KiB
TypeScript

// 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<string, { q: string; cc: string }> = {
'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<string, { lat: number; lon: number; elevation: number; tz: string }> = {
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<void> {
const out: Record<string, { lat: number; lon: number; elevation: number; tz: string }> = {};
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();