import type { Place } from '#shared/types' const UA = 'lecheminvert.tech (adam@belkacemi.me)' interface BanResponse { features: Array<{ geometry: { coordinates: [number, number] } properties: { label: string } }> } interface PhotonResponse { features: Array<{ geometry: { coordinates: [number, number] } properties: { name?: string city?: string country?: string street?: string housenumber?: string } }> } /** La BAN est excellente en France, et aveugle partout ailleurs. */ async function fromBan(q: string): Promise { const url = `https://api-adresse.data.gouv.fr/search/?q=${encodeURIComponent(q)}&limit=5&autocomplete=1` const data = await $fetch(url, { headers: { 'User-Agent': UA } }) return data.features.map(f => ({ label: f.properties.label, lon: f.geometry.coordinates[0], lat: f.geometry.coordinates[1], })) } /** Photon couvre le monde entier via OpenStreetMap, sans clé. */ async function fromPhoton(q: string): Promise { const url = `https://photon.komoot.io/api/?q=${encodeURIComponent(q)}&limit=5&lang=fr` const data = await $fetch(url, { headers: { 'User-Agent': UA } }) return data.features.map((f) => { const p = f.properties const rue = [p.housenumber, p.street].filter(Boolean).join(' ') const parts = [rue || p.name, p.city !== p.name ? p.city : undefined, p.country] return { label: parts.filter(Boolean).join(', '), lon: f.geometry.coordinates[0], lat: f.geometry.coordinates[1], } }) } /** Deux entrees sont identiques si elles partagent le nom ou la position. */ export function dedupe(places: Place[]): Place[] { const seenLabel = new Set() const seenCoord = new Set() return places.filter((p) => { const label = p.label.toLowerCase().trim() const coord = `${p.lat.toFixed(2)},${p.lon.toFixed(2)}` if (seenLabel.has(label) || seenCoord.has(coord)) return false seenLabel.add(label) seenCoord.add(coord) return true }) } /** * Les deux sources sont entrelacees plutot que concatenees : sans cela, * les cinq resultats francais de la BAN enterrent systematiquement la * vraie ville etrangere trouvee par Photon. */ export async function searchPlaces(q: string): Promise { const [ban, photon] = await Promise.allSettled([fromBan(q), fromPhoton(q)]) const a = ban.status === 'fulfilled' ? ban.value : [] const b = photon.status === 'fulfilled' ? photon.value : [] const merged: Place[] = [] for (let i = 0; i < Math.max(a.length, b.length); i++) { if (a[i]) merged.push(a[i]!) if (b[i]) merged.push(b[i]!) } return dedupe(merged).slice(0, 8) }