/** * Script one-shot : ajoute latitude/longitude aux structures depuis Nominatim * Usage : node scripts/geocode-structures.js * IMPORTANT : respecter le rate limit Nominatim (1 req/sec max) * * Stratégie à 3 niveaux : * 1. Lookup statique (villes les plus fréquentes - instantané) * 2. Nominatim API (geocodage réel - 1 req/sec) * 3. Fallback pays (centroïde national si ville inconnue) */ import * as fs from 'fs' import * as path from 'path' import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const DATA_PATH = path.join(__dirname, '..', 'public', 'data', 'reseaux-bifurcation.json') const data = JSON.parse(fs.readFileSync(DATA_PATH, 'utf-8')) // Lookup table statique pour villes connues (évite appels API pour les fréquentes) const CITY_COORDS = { 'Paris': { lat: 48.8566, lng: 2.3522 }, 'Bruxelles': { lat: 50.8503, lng: 4.3517 }, 'Brussels': { lat: 50.8503, lng: 4.3517 }, 'Lyon': { lat: 45.7640, lng: 4.8357 }, 'Marseille': { lat: 43.2965, lng: 5.3698 }, 'Toulouse': { lat: 43.6047, lng: 1.4442 }, 'Bordeaux': { lat: 44.8378, lng: -0.5792 }, 'Nantes': { lat: 47.2184, lng: -1.5536 }, 'Strasbourg': { lat: 48.5734, lng: 7.7521 }, 'Lille': { lat: 50.6292, lng: 3.0573 }, 'Grenoble': { lat: 45.1885, lng: 5.7245 }, 'Montpellier': { lat: 43.6108, lng: 3.8767 }, 'Rennes': { lat: 48.1173, lng: -1.6778 }, 'Berlin': { lat: 52.5200, lng: 13.4050 }, 'Amsterdam': { lat: 52.3676, lng: 4.9041 }, 'London': { lat: 51.5074, lng: -0.1278 }, 'Madrid': { lat: 40.4168, lng: -3.7038 }, 'Barcelona': { lat: 41.3851, lng: 2.1734 }, 'Rome': { lat: 41.9028, lng: 12.4964 }, 'Vienna': { lat: 48.2082, lng: 16.3738 }, 'Warsaw': { lat: 52.2297, lng: 21.0122 }, 'Copenhagen': { lat: 55.6761, lng: 12.5683 }, 'Zurich': { lat: 47.3769, lng: 8.5417 }, 'Helsinki': { lat: 60.1699, lng: 24.9384 }, 'Porto': { lat: 41.1579, lng: -8.6291 }, 'Lisbonne': { lat: 38.7169, lng: -9.1399 }, 'Lisbon': { lat: 38.7169, lng: -9.1399 }, 'Gent': { lat: 51.0543, lng: 3.7174 }, 'Ghent': { lat: 51.0543, lng: 3.7174 }, 'Liège': { lat: 50.6326, lng: 5.5797 }, 'Krakow': { lat: 50.0647, lng: 19.9450 }, 'Wroclaw': { lat: 51.1079, lng: 17.0385 }, 'Lausanne': { lat: 46.5197, lng: 6.6323 }, 'Genève': { lat: 46.2044, lng: 6.1432 }, 'Geneva': { lat: 46.2044, lng: 6.1432 }, 'Namur': { lat: 50.4669, lng: 4.8674 }, 'Tallinn': { lat: 59.4370, lng: 24.7536 }, 'Brest': { lat: 48.3904, lng: -4.4861 }, 'Tours': { lat: 47.3941, lng: 0.6848 }, 'Caen': { lat: 49.1829, lng: -0.3707 }, 'Montauban': { lat: 44.0174, lng: 1.3518 }, 'Bayonne': { lat: 43.4929, lng: -1.4748 }, 'Pau': { lat: 43.2951, lng: -0.3708 }, 'Aurillac': { lat: 44.9282, lng: 2.4480 }, 'Aubenas': { lat: 44.6208, lng: 4.3908 }, 'Hyères': { lat: 43.1200, lng: 6.1283 }, 'Aubagne': { lat: 43.2940, lng: 5.5706 }, 'Auch': { lat: 43.6462, lng: 0.5851 }, 'Clermont-Ferrand': { lat: 45.7797, lng: 3.0863 }, 'Dijon': { lat: 47.3220, lng: 5.0415 }, 'Nice': { lat: 43.7102, lng: 7.2620 }, 'Metz': { lat: 49.1193, lng: 6.1757 }, 'Nancy': { lat: 48.6921, lng: 6.1844 }, 'Rouen': { lat: 49.4432, lng: 1.0993 }, 'Angers': { lat: 47.4784, lng: -0.5632 }, 'Reims': { lat: 49.2583, lng: 4.0317 }, 'Le Mans': { lat: 47.9960, lng: 0.1966 }, 'Amiens': { lat: 49.8941, lng: 2.2958 }, 'Perpignan': { lat: 42.6886, lng: 2.8948 }, 'Orléans': { lat: 47.9029, lng: 1.9039 }, 'Limoges': { lat: 45.8315, lng: 1.2578 }, 'Mulhouse': { lat: 47.7508, lng: 7.3359 }, 'Besançon': { lat: 47.2378, lng: 6.0241 }, 'Poitiers': { lat: 46.5802, lng: 0.3404 }, 'Villeurbanne': { lat: 45.7676, lng: 4.8800 }, 'Aix-en-Provence': { lat: 43.5297, lng: 5.4474 }, 'Boulogne-Billancourt': { lat: 48.8353, lng: 2.2400 }, 'Nîmes': { lat: 43.8367, lng: 4.3601 }, 'Argenteuil': { lat: 48.9478, lng: 2.2476 }, 'Montreuil': { lat: 48.8638, lng: 2.4440 }, 'Roubaix': { lat: 50.6942, lng: 3.1746 }, 'Tourcoing': { lat: 50.7238, lng: 3.1612 }, 'Dunkerque': { lat: 51.0343, lng: 2.3752 }, 'Calais': { lat: 50.9513, lng: 1.8587 }, 'Valenciennes': { lat: 50.3573, lng: 3.5239 }, 'Chartres': { lat: 48.4469, lng: 1.4877 }, 'Colmar': { lat: 48.0778, lng: 7.3585 }, 'Lorient': { lat: 47.7482, lng: -3.3714 }, 'Quimper': { lat: 47.9983, lng: -4.0975 }, 'Vannes': { lat: 47.6559, lng: -2.7602 }, 'Saint-Nazaire': { lat: 47.2736, lng: -2.2137 }, 'La Rochelle': { lat: 46.1603, lng: -1.1511 }, 'Angoulême': { lat: 45.6500, lng: 0.1561 }, 'Périgueux': { lat: 45.1866, lng: 0.7213 }, 'Tulle': { lat: 45.2672, lng: 1.7726 }, 'Guéret': { lat: 46.1714, lng: 1.8714 }, 'Moulins': { lat: 46.5647, lng: 3.3325 }, 'Brive-la-Gaillarde': { lat: 45.1597, lng: 1.5317 }, 'Saint-Étienne': { lat: 45.4397, lng: 4.3872 }, 'Grenoble': { lat: 45.1885, lng: 5.7245 }, 'Valence': { lat: 44.9334, lng: 4.8924 }, 'Chambéry': { lat: 45.5646, lng: 5.9178 }, 'Annecy': { lat: 45.8992, lng: 6.1294 }, 'Mâcon': { lat: 46.3066, lng: 4.8281 }, 'Chalon-sur-Saône': { lat: 46.7806, lng: 4.8534 }, 'Auxerre': { lat: 47.7977, lng: 3.5740 }, 'Troyes': { lat: 48.2973, lng: 4.0744 }, 'Châlons-en-Champagne': { lat: 48.9571, lng: 4.3665 }, 'Épinal': { lat: 48.1741, lng: 6.4490 }, 'Belfort': { lat: 47.6396, lng: 6.8633 }, 'Montbéliard': { lat: 47.5076, lng: 6.7987 }, 'Vilvorde': { lat: 50.9267, lng: 4.4145 }, 'Liège': { lat: 50.6326, lng: 5.5797 }, 'Louvain-la-Neuve': { lat: 50.6683, lng: 4.6118 }, 'Leuven': { lat: 50.8798, lng: 4.7005 }, 'Antwerp': { lat: 51.2194, lng: 4.4025 }, 'Anvers': { lat: 51.2194, lng: 4.4025 }, 'Rotterdam': { lat: 51.9244, lng: 4.4777 }, 'The Hague': { lat: 52.0705, lng: 4.3007 }, 'Utrecht': { lat: 52.0907, lng: 5.1214 }, 'Eindhoven': { lat: 51.4416, lng: 5.4697 }, 'Tilburg': { lat: 51.5555, lng: 5.0913 }, 'Hamburg': { lat: 53.5753, lng: 10.0153 }, 'Munich': { lat: 48.1351, lng: 11.5820 }, 'Cologne': { lat: 50.9333, lng: 6.9500 }, 'Frankfurt': { lat: 50.1109, lng: 8.6821 }, 'Stuttgart': { lat: 48.7758, lng: 9.1829 }, 'Dusseldorf': { lat: 51.2217, lng: 6.7762 }, 'Dortmund': { lat: 51.5136, lng: 7.4653 }, 'Essen': { lat: 51.4556, lng: 7.0116 }, 'Leipzig': { lat: 51.3397, lng: 12.3731 }, 'Dresden': { lat: 51.0504, lng: 13.7373 }, 'Hanover': { lat: 52.3759, lng: 9.7320 }, 'Nuremberg': { lat: 49.4521, lng: 11.0767 }, 'Freiburg': { lat: 47.9990, lng: 7.8421 }, 'Kassel': { lat: 51.3127, lng: 9.4797 }, 'Mannheim': { lat: 49.4875, lng: 8.4660 }, 'Heidelberg': { lat: 49.3988, lng: 8.6724 }, 'Tübingen': { lat: 48.5217, lng: 9.0576 }, 'Weimar': { lat: 50.9795, lng: 11.3235 }, 'Bristol': { lat: 51.4545, lng: -2.5879 }, 'Manchester': { lat: 53.4808, lng: -2.2426 }, 'Birmingham': { lat: 52.4862, lng: -1.8904 }, 'Edinburgh': { lat: 55.9533, lng: -3.1883 }, 'Glasgow': { lat: 55.8642, lng: -4.2518 }, 'Leeds': { lat: 53.7997, lng: -1.5492 }, 'Malmö': { lat: 55.6050, lng: 13.0038 }, 'Stockholm': { lat: 59.3293, lng: 18.0686 }, 'Göteborg': { lat: 57.7089, lng: 11.9746 }, 'Oslo': { lat: 59.9139, lng: 10.7522 }, 'Bergen': { lat: 60.3929, lng: 5.3241 }, 'Lausanne': { lat: 46.5197, lng: 6.6323 }, 'Basel': { lat: 47.5596, lng: 7.5886 }, 'Bern': { lat: 46.9480, lng: 7.4474 }, 'Milan': { lat: 45.4654, lng: 9.1859 }, 'Turin': { lat: 45.0703, lng: 7.6869 }, 'Florence': { lat: 43.7696, lng: 11.2558 }, 'Naples': { lat: 40.8518, lng: 14.2681 }, 'Bologna': { lat: 44.4949, lng: 11.3426 }, 'Venice': { lat: 45.4408, lng: 12.3155 }, 'Venise': { lat: 45.4408, lng: 12.3155 }, 'Séville': { lat: 37.3891, lng: -5.9845 }, 'Seville': { lat: 37.3891, lng: -5.9845 }, 'Valence': { lat: 39.4699, lng: -0.3763 }, 'Bilbao': { lat: 43.2630, lng: -2.9350 }, 'Casablanca': { lat: 33.5731, lng: -7.5898 }, 'Rabat': { lat: 34.0209, lng: -6.8417 }, 'Dakar': { lat: 14.7167, lng: -17.4677 }, 'Abidjan': { lat: 5.3600, lng: -4.0083 }, 'Nairobi': { lat: -1.2921, lng: 36.8219 }, 'Tunis': { lat: 36.8065, lng: 10.1815 }, 'Alger': { lat: 36.7372, lng: 3.0864 }, 'New York': { lat: 40.7128, lng: -74.0060 }, 'San Francisco': { lat: 37.7749, lng: -122.4194 }, 'Los Angeles': { lat: 34.0522, lng: -118.2437 }, 'Chicago': { lat: 41.8781, lng: -87.6298 }, 'Boston': { lat: 42.3601, lng: -71.0589 }, 'Seattle': { lat: 47.6062, lng: -122.3321 }, 'Montreal': { lat: 45.5017, lng: -73.5673 }, 'Toronto': { lat: 43.6532, lng: -79.3832 }, 'Vancouver': { lat: 49.2827, lng: -123.1207 }, 'Melbourne': { lat: -37.8136, lng: 144.9631 }, 'Sydney': { lat: -33.8688, lng: 151.2093 }, 'Tokyo': { lat: 35.6762, lng: 139.6503 }, 'Seoul': { lat: 37.5665, lng: 126.9780 }, 'Shanghai': { lat: 31.2304, lng: 121.4737 }, 'Beijing': { lat: 39.9042, lng: 116.4074 }, 'Mexico City': { lat: 19.4326, lng: -99.1332 }, 'Buenos Aires': { lat: -34.6118, lng: -58.3960 }, 'São Paulo': { lat: -23.5505, lng: -46.6333 }, 'Rio de Janeiro': { lat: -22.9068, lng: -43.1729 }, } // Fallback par pays si ville inconnue const COUNTRY_FALLBACK = { 'FR': { lat: 46.6034, lng: 1.8883 }, 'BE': { lat: 50.5039, lng: 4.4699 }, 'NL': { lat: 52.3702, lng: 4.8952 }, 'DE': { lat: 51.1657, lng: 10.4515 }, 'GB': { lat: 55.3781, lng: -3.4360 }, 'UK': { lat: 55.3781, lng: -3.4360 }, 'IT': { lat: 41.8719, lng: 12.5674 }, 'ES': { lat: 40.4637, lng: -3.7492 }, 'CH': { lat: 46.8182, lng: 8.2275 }, 'PL': { lat: 51.9194, lng: 19.1451 }, 'DK': { lat: 56.2639, lng: 9.5018 }, 'AT': { lat: 47.5162, lng: 14.5501 }, 'PT': { lat: 39.3999, lng: -8.2245 }, 'FI': { lat: 61.9241, lng: 25.7482 }, 'SE': { lat: 60.1282, lng: 18.6435 }, 'NO': { lat: 60.4720, lng: 8.4689 }, 'US': { lat: 37.0902, lng: -95.7129 }, 'MA': { lat: 31.7917, lng: -7.0926 }, 'SN': { lat: 14.4974, lng: -14.4524 }, 'MG': { lat: -18.7669, lng: 46.8691 }, 'EE': { lat: 58.5953, lng: 25.0136 }, 'LT': { lat: 55.1694, lng: 23.8813 }, 'LV': { lat: 56.8796, lng: 24.6032 }, 'HU': { lat: 47.1625, lng: 19.5033 }, 'CZ': { lat: 49.8175, lng: 15.4730 }, 'SK': { lat: 48.6690, lng: 19.6990 }, 'RO': { lat: 45.9432, lng: 24.9668 }, 'GR': { lat: 39.0742, lng: 21.8243 }, 'HR': { lat: 45.1000, lng: 15.2000 }, 'SI': { lat: 46.1512, lng: 14.9955 }, 'RS': { lat: 44.0165, lng: 21.0059 }, 'CA': { lat: 56.1304, lng: -106.3468 }, 'AU': { lat: -25.2744, lng: 133.7751 }, 'NZ': { lat: -40.9006, lng: 174.8860 }, 'JP': { lat: 36.2048, lng: 138.2529 }, 'KR': { lat: 35.9078, lng: 127.7669 }, 'CN': { lat: 35.8617, lng: 104.1954 }, 'BR': { lat: -14.2350, lng: -51.9253 }, 'AR': { lat: -38.4161, lng: -63.6167 }, 'MX': { lat: 23.6345, lng: -102.5528 }, 'CL': { lat: -35.6751, lng: -71.5430 }, 'CO': { lat: 4.5709, lng: -74.2973 }, 'PE': { lat: -9.1900, lng: -75.0152 }, 'TN': { lat: 33.8869, lng: 9.5375 }, 'DZ': { lat: 28.0339, lng: 1.6596 }, 'CI': { lat: 7.5399, lng: -5.5471 }, 'KE': { lat: -0.0236, lng: 37.9062 }, 'ZA': { lat: -30.5595, lng: 22.9375 }, 'NG': { lat: 9.0820, lng: 8.6753 }, 'GH': { lat: 7.9465, lng: -1.0232 }, 'CM': { lat: 3.8480, lng: 11.5021 }, 'ET': { lat: 9.1450, lng: 40.4897 }, 'TZ': { lat: -6.3690, lng: 34.8888 }, 'UG': { lat: 1.3733, lng: 32.2903 }, 'RW': { lat: -1.9403, lng: 29.8739 }, 'IN': { lat: 20.5937, lng: 78.9629 }, 'BD': { lat: 23.6850, lng: 90.3563 }, 'PK': { lat: 30.3753, lng: 69.3451 }, 'ID': { lat: -0.7893, lng: 113.9213 }, 'TH': { lat: 15.8700, lng: 100.9925 }, 'VN': { lat: 14.0583, lng: 108.2772 }, 'MY': { lat: 4.2105, lng: 101.9758 }, 'PH': { lat: 12.8797, lng: 121.7740 }, } async function geocodeNominatim(ville, pays) { await new Promise(r => setTimeout(r, 1100)) // rate limit 1 req/sec try { const query = encodeURIComponent(`${ville}, ${pays}`) const url = `https://nominatim.openstreetmap.org/search?q=${query}&format=json&limit=1` const res = await fetch(url, { headers: { 'User-Agent': 'AEP-Bifurcation-Map/1.0' } }) const result = await res.json() if (result.length > 0) { return { lat: parseFloat(result[0].lat), lng: parseFloat(result[0].lon) } } } catch (e) { console.warn(`Geocoding failed for ${ville}, ${pays}: ${e.message}`) } return null } async function main() { let enriched = 0 let fromCache = 0 let fromNominatim = 0 let fromCountryFallback = 0 let failed = 0 console.log(`Starting geocoding of ${data.structures.length} structures...`) for (const structure of data.structures) { // Déjà geocodé if (structure.latitude != null && structure.longitude != null) { enriched++ continue } const villeKey = structure.ville?.trim() // 1. Lookup statique if (villeKey && CITY_COORDS[villeKey]) { structure.latitude = CITY_COORDS[villeKey].lat structure.longitude = CITY_COORDS[villeKey].lng enriched++ fromCache++ process.stdout.write('.') continue } // 2. Nominatim if (villeKey && structure.pays) { const coords = await geocodeNominatim(villeKey, structure.pays) if (coords) { structure.latitude = coords.lat structure.longitude = coords.lng enriched++ fromNominatim++ console.log(`\n Geocoded: ${structure.nom} -> ${villeKey} (${coords.lat.toFixed(3)}, ${coords.lng.toFixed(3)})`) continue } } // 3. Fallback pays if (structure.pays && COUNTRY_FALLBACK[structure.pays]) { structure.latitude = COUNTRY_FALLBACK[structure.pays].lat structure.longitude = COUNTRY_FALLBACK[structure.pays].lng enriched++ fromCountryFallback++ process.stdout.write('~') continue } console.warn(`\n No coords for: ${structure.nom} (${structure.ville}, ${structure.pays})`) structure.latitude = null structure.longitude = null failed++ } console.log('\n') fs.writeFileSync(DATA_PATH, JSON.stringify(data, null, 2), 'utf-8') console.log(`Done: ${enriched}/${data.structures.length} structures geocoded`) console.log(` - Cache statique : ${fromCache}`) console.log(` - Nominatim API : ${fromNominatim}`) console.log(` - Fallback pays : ${fromCountryFallback}`) console.log(` - Echecs : ${failed}`) } main().catch(console.error)