import { httpExternal } from '~/shared/utils/httpExternal' // Autocompletion d'adresse branchee sur la Base Adresse Nationale (BAN), // `api-adresse.data.gouv.fr` — service public francais, gratuit, CORS ouvert. // // Appel HTTP DIRECT depuis le front (pas de proxy back), conformement a la spec // M1 (§ API adresse postale). On passe par `httpExternal` et NON `useApi()` : // la BAN est un domaine externe, sans cookie de session ni enveloppe Hydra. // // Contrat (fige) : // searchCity(postalCode) -> liste { city, postalCode } // searchAddress(query, cp?) -> liste { label, street, postalCode, city } // En cas d'erreur/timeout, la methode THROW une AddressAutocompleteUnavailableError. // Le composant consommateur catch, affiche un toast d'avertissement et bascule // en saisie libre (MalioInputText). /** URL de l'endpoint de recherche BAN. */ const BAN_SEARCH_URL = 'https://api-adresse.data.gouv.fr/search/' /** Une suggestion de ville renvoyee a partir d'un code postal. */ export interface CitySuggestion { city: string postalCode: string } /** Une suggestion d'adresse complete (saisie assistee du champ « Adresse »). */ export interface AddressSuggestion { label: string street: string postalCode: string city: string } export interface AddressAutocomplete { searchCity(postalCode: string): Promise searchAddress(query: string, postalCode?: string): Promise } /** Erreur signalant que le service d'autocompletion BAN n'est pas disponible. */ export class AddressAutocompleteUnavailableError extends Error { constructor() { // Message technique (non affiche tel quel) : le composant remonte son // propre libelle i18n. Sert au debug / aux logs uniquement. super('Address autocomplete (BAN) is not available.') this.name = 'AddressAutocompleteUnavailableError' } } /** Proprietes d'une « feature » GeoJSON renvoyee par la BAN (champs utilises). */ interface BanFeatureProperties { label?: string name?: string street?: string postcode?: string city?: string } /** Reponse GeoJSON FeatureCollection de la BAN. */ interface BanResponse { features?: { properties?: BanFeatureProperties }[] } export function useAddressAutocomplete(): AddressAutocomplete { return { async searchCity(postalCode: string): Promise { let res: BanResponse try { res = await httpExternal(BAN_SEARCH_URL, { query: { q: postalCode, type: 'municipality' }, }) } catch { // Reseau coupe, 5xx, timeout... -> mode degrade cote composant. throw new AddressAutocompleteUnavailableError() } return (res.features ?? []).map((feature) => { const props = feature.properties ?? {} return { city: props.city ?? props.name ?? '', postalCode: props.postcode ?? '', } }) }, async searchAddress(query: string, postalCode?: string): Promise { // IMPORTANT : pas de `type=housenumber` ici. La BAN ne renvoie un // resultat de ce type qu'une fois un numero saisi → une recherche par // nom de rue (« boulevard du port ») renverrait 0 resultat pendant // toute la frappe. Sans filtre `type`, la BAN classe rues + numeros // par pertinence (comportement d'autocompletion attendu). // On n'ajoute `postcode` que s'il est fourni (sinon recherche large). const banQuery: Record = { q: query } if (postalCode) { banQuery.postcode = postalCode } let res: BanResponse try { res = await httpExternal(BAN_SEARCH_URL, { query: banQuery }) } catch { throw new AddressAutocompleteUnavailableError() } return (res.features ?? []).map((feature) => { const props = feature.properties ?? {} return { label: props.label ?? '', // `name` porte la ligne d'adresse complete (numero + voie) ; // `street` ne contient que la voie. On privilegie `name`. street: props.name ?? props.street ?? '', postalCode: props.postcode ?? '', city: props.city ?? '', } }) }, } }