82 lines
No EOL
2.7 KiB
TypeScript
82 lines
No EOL
2.7 KiB
TypeScript
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<Place[]> {
|
|
const url = `https://api-adresse.data.gouv.fr/search/?q=${encodeURIComponent(q)}&limit=5&autocomplete=1`
|
|
const data = await $fetch<BanResponse>(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<Place[]> {
|
|
const url = `https://photon.komoot.io/api/?q=${encodeURIComponent(q)}&limit=5&lang=fr`
|
|
const data = await $fetch<PhotonResponse>(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<string>()
|
|
const seenCoord = new Set<string>()
|
|
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<Place[]> {
|
|
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)
|
|
} |