import type { Association, AssociationDirectoryEntry, InsertAssociationDirectoryEntry } from "../drizzle/schema"; import type { AssociationGeoPrecision, AssociationGeoSource } from "@shared/associationGeo"; type CoordinateSet = { latitude: string | null; longitude: string | null; geoSource: AssociationGeoSource | null; externalSourceStatus: string; externalSourceLabel: string | null; }; function formatCoordinate(value: number | null) { if (value === null || Number.isNaN(value)) return null; return value.toFixed(6); } function buildAddressQuery(entry: AssociationDirectoryEntry, association?: Association | null) { const adresse = association?.adresse || entry.adresse; const codePostal = association?.codePostal || entry.codePostal; const ville = association?.ville || entry.ville; return [adresse, codePostal, ville].filter(Boolean).join(" ").trim(); } async function geocodeAddress(query: string) { const url = new URL("https://api-adresse.data.gouv.fr/search/"); url.searchParams.set("q", query); url.searchParams.set("limit", "1"); const response = await fetch(url, { headers: { "Accept": "application/json", "User-Agent": "portail-associations/1.0", }, }); if (!response.ok) { throw new Error(`Adresse API returned ${response.status}`); } const payload = await response.json() as { features?: Array<{ geometry?: { coordinates?: [number, number] }; properties?: { label?: string }; }>; }; const first = payload.features?.[0]; const coordinates = first?.geometry?.coordinates; if (!coordinates || coordinates.length < 2) { return null; } return { latitude: coordinates[1], longitude: coordinates[0], label: first?.properties?.label || null, }; } async function fetchCommuneCenter(ville: string, codePostal?: string | null) { const url = new URL("https://geo.api.gouv.fr/communes"); url.searchParams.set("nom", ville); url.searchParams.set("fields", "nom,centre,code,codesPostaux"); url.searchParams.set("format", "json"); url.searchParams.set("geometry", "centre"); if (codePostal) { url.searchParams.set("codePostal", codePostal); } const response = await fetch(url, { headers: { "Accept": "application/json", "User-Agent": "portail-associations/1.0", }, }); if (!response.ok) { throw new Error(`Geo API returned ${response.status}`); } const payload = await response.json() as Array<{ nom?: string; centre?: { coordinates?: [number, number] }; }>; const first = payload[0]; const coordinates = first?.centre?.coordinates; if (!coordinates || coordinates.length < 2) { return null; } return { latitude: coordinates[1], longitude: coordinates[0], label: first?.nom || ville, }; } export async function computeAssociationDirectoryGeoUpdate(entry: AssociationDirectoryEntry, association?: Association | null) { return computeAssociationDirectoryGeoUpdateForPrecision( entry, association, (entry.geoPrecision as AssociationGeoPrecision | null) || "commune_center" ); } export async function computeAssociationDirectoryGeoUpdateForPrecision( entry: AssociationDirectoryEntry, association: Association | null | undefined, preferredPrecision: AssociationGeoPrecision ) { const updates: Partial = { geoLastSyncedAt: new Date(), geoPrecision: preferredPrecision, }; const addressQuery = buildAddressQuery(entry, association); if (preferredPrecision === "exact_address" && addressQuery) { try { const geocoded = await geocodeAddress(addressQuery); if (geocoded) { updates.latitude = formatCoordinate(geocoded.latitude); updates.longitude = formatCoordinate(geocoded.longitude); updates.geoSource = "adresse_gouv"; updates.externalSourceStatus = "geocoded_from_address"; updates.externalSourceLabel = geocoded.label || "Adresse.data.gouv.fr"; return updates; } } catch { // Fall back to commune center below. } } const ville = association?.ville || entry.ville; const codePostal = association?.codePostal || entry.codePostal; if (ville) { try { const communeCenter = await fetchCommuneCenter(ville, codePostal); if (communeCenter) { updates.latitude = formatCoordinate(communeCenter.latitude); updates.longitude = formatCoordinate(communeCenter.longitude); updates.geoSource = "commune_center"; updates.externalSourceStatus = "commune_center_fallback"; updates.externalSourceLabel = communeCenter.label || ville; return updates; } } catch { // Fall through to unavailable state. } } updates.latitude = null; updates.longitude = null; updates.geoSource = null; updates.externalSourceStatus = "unresolved"; updates.externalSourceLabel = null; return updates; } export function getPublicMapCoordinates(entry: AssociationDirectoryEntry) { if (!entry.latitude || !entry.longitude) { return null; } const latitude = Number(entry.latitude); const longitude = Number(entry.longitude); if (Number.isNaN(latitude) || Number.isNaN(longitude)) { return null; } return { latitude, longitude, precision: ( entry.geoPrecision === "hidden" ? (entry.geoSource === "commune_center" ? "commune_center" : "exact_address") : entry.geoPrecision ) as AssociationGeoPrecision, }; }