> Propositions normalisées, les * plus pertinentes d'abord (ordre de score rendu par la BAN). * * @throws RuntimeException Saisie trop courte, ou API injoignable. */ public function search(string $query, int $limit = 5): array { $query = trim(preg_replace('/\s+/u', ' ', $query) ?? $query); $length = function_exists('mb_strlen') ? mb_strlen($query, 'UTF-8') : strlen($query); if ($length < self::MIN_QUERY_LENGTH) { throw new RuntimeException('Saisissez au moins ' . self::MIN_QUERY_LENGTH . ' caractères.'); } $limit = max(1, min(self::MAX_LIMIT, $limit)); $url = self::ENDPOINT . '?' . http_build_query([ 'q' => $query, 'limit' => $limit, 'autocomplete' => 1, ], '', '&', PHP_QUERY_RFC3986); $payload = $this->fetchJson($url); $features = $payload['features'] ?? null; if (!is_array($features)) { throw new RuntimeException('Réponse inattendue de l’API Adresse.'); } $suggestions = []; foreach ($features as $feature) { $suggestion = $this->normalizeFeature($feature); if ($suggestion !== null) { $suggestions[] = $suggestion; } } return $suggestions; } /** * Convertit une « feature » GeoJSON de la BAN en proposition exploitable. * * `label` est l'adresse complète telle qu'elle sera écrite dans la fiche * agent ; `nom` et `contexte` ne servent qu'à l'affichage de la liste. * Une feature sans label est ignorée : sans elle il n'y a rien à insérer. * * @return array|null */ private function normalizeFeature(mixed $feature): ?array { if (!is_array($feature) || !is_array($feature['properties'] ?? null)) { return null; } $properties = $feature['properties']; $label = trim((string) ($properties['label'] ?? '')); if ($label === '') { return null; } return [ 'label' => $label, 'nom' => trim((string) ($properties['name'] ?? '')), 'code_postal' => trim((string) ($properties['postcode'] ?? '')), 'commune' => trim((string) ($properties['city'] ?? '')), 'contexte' => trim((string) ($properties['context'] ?? '')), // housenumber | street | locality | municipality : une proposition // « municipality » n'est qu'une commune, pas une adresse précise. 'type' => trim((string) ($properties['type'] ?? '')), ]; } /** * Requête HTTP + décodage JSON, sur le modèle de SchoolHolidayApiService : * cURL quand l'extension est là, repli sur file_get_contents() sinon. * * Délais VOLONTAIREMENT COURTS (4 s de connexion, 8 s au total) : l'appel * est déclenché à la frappe, un utilisateur ne patiente pas devant une * liste de suggestions. Mieux vaut renoncer que faire attendre. * * @return array * * @throws RuntimeException Erreur réseau, code HTTP >= 400, ou JSON invalide. */ private function fetchJson(string $url): array { $body = false; $status = 0; if (function_exists('curl_init')) { $curl = curl_init($url); if ($curl === false) { throw new RuntimeException('Impossible d’initialiser cURL.'); } curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => 4, CURLOPT_TIMEOUT => 8, CURLOPT_ENCODING => '', CURLOPT_HTTPHEADER => ['Accept: application/json', 'User-Agent: PTA/7.26'], ]); $body = curl_exec($curl); $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE); $error = curl_error($curl); curl_close($curl); if ($body === false) { throw new RuntimeException($error ?: 'Erreur réseau cURL.'); } } else { $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => 8, 'ignore_errors' => true, 'header' => "Accept: application/json\r\nUser-Agent: PTA/7.26\r\n", ], ]); $body = @file_get_contents($url, false, $context); if ($body === false) { throw new RuntimeException('La lecture d’URL distante est désactivée sur le serveur PHP. Activez cURL ou allow_url_fopen.'); } foreach ($http_response_header ?? [] as $header) { if (preg_match('/^HTTP\/\S+\s+(\d{3})/', $header, $matches)) { $status = (int) $matches[1]; break; } } } if ($status >= 400) { throw new RuntimeException('L’API Adresse a répondu avec le code HTTP ' . $status . '.'); } $data = json_decode((string) $body, true); if (!is_array($data)) { throw new RuntimeException('Le contenu reçu depuis l’API Adresse n’est pas un JSON valide.'); } return $data; } }