#!/usr/bin/env node /** * Curation du corpus street (« An lari a ») — mode Mapillary. * « Fetch, pas recall » : pour chaque commune cible, interroge l'API Mapillary * (bbox autour du centre communal, préférence 360° + qualité + récence), * geo.api.gouv.fr (centre, superficie), api-adresse (nom de rue) et Wikidata * (QID de la commune via le sitelink frwiki). * * Usage : MAPILLARY_TOKEN=… node scripts/curate-street.mjs * (le token est aussi lu depuis .env : MAPILLARY_TOKEN ou PUBLIC_MAPILLARY_TOKEN) * Écrit src/lib/server/data/street.json. */ import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); function chargeEnv() { try { for (const ligne of readFileSync(join(ROOT, '.env'), 'utf8').split('\n')) { const m = ligne.match(/^\s*([A-Z_]+)\s*=\s*(.+?)\s*$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2]; } } catch { /* pas de .env */ } } chargeEnv(); const TOKEN = process.env.MAPILLARY_TOKEN || process.env.PUBLIC_MAPILLARY_TOKEN; if (!TOKEN) { console.error('Token Mapillary manquant (MAPILLARY_TOKEN ou PUBLIC_MAPILLARY_TOKEN).'); process.exit(1); } const UA = { 'User-Agent': 'JWE-OKI/2.0 (curation corpus street; contact: o-k-i.net)' }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const getJSON = async (url, headers = UA) => { const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`HTTP ${res.status} sur ${url}`); return res.json(); }; // Communes cibles : 5 par région, difficulté = précision exigée (pas de FACILE : // rayon 50 km = trivial en mode street). titreWiki = titre frwiki exact (désambiguïsé). const SPOTS = [ // — Guadeloupe (971) — { region: 'GUADELOUPE', commune: 'Pointe-à-Pitre', titreWiki: 'Pointe-à-Pitre', difficulte: 'MOYEN', indice1: 'Île papillon, aile droite', indice2: 'Port de commerce principal de l’archipel' }, { region: 'GUADELOUPE', commune: 'Les Abymes', titreWiki: 'Les Abymes', difficulte: 'MOYEN', indice1: 'Île papillon, aile droite', indice2: 'Commune la plus peuplée de l’archipel' }, { region: 'GUADELOUPE', commune: 'Le Gosier', titreWiki: 'Le Gosier', difficulte: 'DIFFICILE', indice1: 'Île papillon, aile droite', indice2: 'Station balnéaire au sud de la Grande-Terre' }, { region: 'GUADELOUPE', commune: 'Sainte-Anne', titreWiki: 'Sainte-Anne (Guadeloupe)', difficulte: 'DIFFICILE', indice1: 'Île papillon, aile droite', indice2: 'Plages au sud de la Grande-Terre' }, { region: 'GUADELOUPE', commune: 'Basse-Terre', titreWiki: 'Basse-Terre', difficulte: 'EXPERT', indice1: 'Île papillon, aile gauche', indice2: 'Chef-lieu au pied du volcan' }, // — Martinique (972) — { region: 'MARTINIQUE', commune: 'Fort-de-France', titreWiki: 'Fort-de-France', difficulte: 'MOYEN', indice1: 'Île aux fleurs', indice2: 'Chef-lieu, sur une grande baie' }, { region: 'MARTINIQUE', commune: 'Le Lamentin', titreWiki: 'Le Lamentin', difficulte: 'MOYEN', indice1: 'Île aux fleurs', indice2: 'Banlieue du chef-lieu, zone aéroportuaire' }, { region: 'MARTINIQUE', commune: 'Saint-Pierre', titreWiki: 'Saint-Pierre (Martinique)', difficulte: 'DIFFICILE', indice1: 'Île aux fleurs, côte caraïbe nord', indice2: 'Ville détruite par une éruption en 1902' }, { region: 'MARTINIQUE', commune: 'Le François', titreWiki: 'Le François', difficulte: 'DIFFICILE', indice1: 'Île aux fleurs, côte atlantique', indice2: 'Bourg face aux îlets du sud' }, { region: 'MARTINIQUE', commune: 'Sainte-Marie', titreWiki: 'Sainte-Marie (Martinique)', difficulte: 'EXPERT', indice1: 'Île aux fleurs, côte atlantique nord', indice2: 'Musée de la banane et rhum' }, // — Guyane (973) — { region: 'GUYANE', commune: 'Cayenne', titreWiki: 'Cayenne', difficulte: 'MOYEN', indice1: 'Terre de forêt amazonienne', indice2: 'Chef-lieu, sur l’île du même nom' }, { region: 'GUYANE', commune: 'Kourou', titreWiki: 'Kourou', difficulte: 'DIFFICILE', indice1: 'Terre de forêt amazonienne', indice2: 'Ville des fusées' }, { region: 'GUYANE', commune: 'Saint-Laurent-du-Maroni', titreWiki: 'Saint-Laurent-du-Maroni', difficulte: 'DIFFICILE', indice1: 'Terre de forêt amazonienne, à l’ouest', indice2: 'Ville du fleuve Maroni, ancien bagne' }, { region: 'GUYANE', commune: 'Matoury', titreWiki: 'Matoury', difficulte: 'EXPERT', indice1: 'Terre de forêt amazonienne', indice2: 'Banlieue sud du chef-lieu' }, { region: 'GUYANE', commune: 'Remire-Montjoly', titreWiki: 'Remire-Montjoly', difficulte: 'EXPERT', indice1: 'Terre de forêt amazonienne', indice2: 'Plages à l’est du chef-lieu' }, // — La Réunion (974) — { region: 'REUNION', commune: 'Saint-Denis', titreWiki: 'Saint-Denis (La Réunion)', difficulte: 'MOYEN', indice1: 'Île intense de l’océan Indien', indice2: 'Chef-lieu, au nord' }, { region: 'REUNION', commune: 'Saint-Pierre', titreWiki: 'Saint-Pierre (La Réunion)', difficulte: 'MOYEN', indice1: 'Île intense de l’océan Indien', indice2: 'Capitale du sud, front de mer animé' }, { region: 'REUNION', commune: 'Le Tampon', titreWiki: 'Le Tampon', difficulte: 'DIFFICILE', indice1: 'Île intense de l’océan Indien', indice2: 'Commune des hauts, dans le sud' }, { region: 'REUNION', commune: 'Saint-Paul', titreWiki: 'Saint-Paul (La Réunion)', difficulte: 'DIFFICILE', indice1: 'Île intense de l’océan Indien', indice2: 'Côte ouest, premier site de peuplement' }, { region: 'REUNION', commune: 'Saint-André', titreWiki: 'Saint-André (La Réunion)', difficulte: 'EXPERT', indice1: 'Île intense de l’océan Indien', indice2: 'Côte est, pays de la canne' } ]; const PREFIXE_REGION = { GUADELOUPE: '971', MARTINIQUE: '972', GUYANE: '973', REUNION: '974' }; async function centreCommune(spot) { const data = await getJSON( `https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(spot.commune)}&codeDepartement=${PREFIXE_REGION[spot.region]}&fields=centre,surface,code,mairie&limit=8` ); const c = data[0]; if (!c) throw new Error(`commune introuvable sur geo.api.gouv.fr`); // La mairie est un meilleur ancrage que le centroïde (les communes DOM // englobent des reliefs : le centroïde tombe souvent hors du centre-ville). const p = c.mairie?.coordinates ? c.mairie.coordinates : c.centre.coordinates; return { lat: p[1], lon: p[0], surfaceHa: c.surface }; } async function chercherImage(lat, lon) { const champs = 'id,is_pano,captured_at,computed_geometry,geometry,creator,quality_score'; const note = (img) => (img.quality_score ?? 0.5) * 1e13 + (img.captured_at ?? 0); // 1) bbox autour de l'ancre (< 0,01° de côté, contrainte API), pano d'abord const demi = 0.0045; const bbox = `${lon - demi},${lat - demi},${lon + demi},${lat + demi}`; for (const pano of ['&is_pano=true', '']) { const data = await getJSON( `https://graph.mapillary.com/images?access_token=${encodeURIComponent(TOKEN)}&fields=${champs}&bbox=${bbox}&limit=200${pano}` ); if (data.data?.length) { const triees = [...data.data].sort((a, b) => note(b) - note(a)); return { image: triees[0], total: data.data.length }; } } // 2) grille de recherches par rayon (50 m) autour de l'ancre const parId = new Map(); for (const dLat of [-0.004, 0, 0.004]) { for (const dLon of [-0.004, 0, 0.004]) { try { const data = await getJSON( `https://graph.mapillary.com/images?access_token=${encodeURIComponent(TOKEN)}&fields=${champs}&lat=${lat + dLat}&lng=${lon + dLon}&radius=50&limit=10` ); for (const img of data.data ?? []) parId.set(img.id, img); } catch { /* point sans couverture */ } await sleep(120); } } if (!parId.size) return null; const triees = [...parId.values()].sort((a, b) => note(b) - note(a)); return { image: triees[0], total: parId.size }; } async function nomDeRue(lat, lon) { try { const data = await getJSON(`https://api-adresse.data.gouv.fr/reverse/?lon=${lon}&lat=${lat}&type=street`); const f = data.features?.[0]?.properties; return f?.name ?? f?.street ?? null; } catch { return null; } } // QIDs des communes via sitelinks frwiki (un seul appel batch) async function qidsCommunes() { const titres = SPOTS.map((s) => s.titreWiki); const data = await getJSON( `https://www.wikidata.org/w/api.php?action=wbgetentities&sites=frwiki&titles=${encodeURIComponent(titres.join('|'))}&props=sitelinks&sitefilter=frwiki&format=json&redirects=yes` ); const parTitre = {}; for (const [qid, e] of Object.entries(data.entities ?? {})) { if (e.sitelinks?.frwiki?.title) parTitre[e.sitelinks.frwiki.title] = qid; } const redirige = {}; for (const r of data.redirects ?? []) redirige[r.from] = r.to; const res = {}; for (const s of SPOTS) { const t = redirige[s.titreWiki] ?? s.titreWiki; res[s.titreWiki] = parTitre[t] ?? null; } return res; } const qids = await qidsCommunes(); const lieux = []; let n = 0; for (const spot of SPOTS) { n++; try { const centre = await centreCommune(spot); await sleep(150); const trouve = await chercherImage(centre.lat, centre.lon); if (!trouve) { console.log(`VIDE ${spot.commune} (${spot.region}) — aucune image dans la bbox`); continue; } const img = trouve.image; const geo = img.computed_geometry ?? img.geometry; const coords = { lat: geo.coordinates[1], lon: geo.coordinates[0] }; await sleep(150); const rue = await nomDeRue(coords.lat, coords.lon); const qid = qids[spot.titreWiki]; lieux.push({ id: `st-${String(n).padStart(2, '0')}`, nom: rue ? `${rue}, ${spot.commune}` : `Centre de ${spot.commune}`, nom_creole: `Lari ${spot.commune}`, region: spot.region, commune: spot.commune, coordonnees: coords, difficulte: spot.difficulte, mapillary_id: img.id, credit: { username: img.creator?.username ?? 'contributeur Mapillary', captured_at: new Date(img.captured_at).toISOString().slice(0, 10) }, is_pano: !!img.is_pano, wikidata_id: qid, indices: [ { niveau: 1, texte: spot.indice1 }, { niveau: 2, texte: spot.indice2 } ], aire_commune_km2: Math.round(centre.surfaceHa) / 100 // geo.api : hectares → km² }); console.log( `OK ${spot.commune.padEnd(24)} img ${img.id} pano=${img.is_pano ? 'oui' : 'NON'} q=${img.quality_score?.toFixed(2) ?? '—'} pool=${trouve.total} qid=${qid ?? 'ABSENT'} rue=${rue ?? '—'}` ); } catch (e) { console.log(`ERREUR ${spot.commune} — ${e.message}`); } await sleep(150); } writeFileSync(join(ROOT, 'src/lib/server/data/street.json'), JSON.stringify(lieux, null, 2) + '\n'); console.log(`\n${lieux.length} lieux écrits dans src/lib/server/data/street.json`);