CREATE OR REPLACE FUNCTION public.search_addresses(search_term text, result_limit integer DEFAULT 10) RETURNS TABLE(identifiant_unique_adresse text, adresse_formatee text, numero_municipal text, numero_unite text, code_postal text, odonyme_recompose_normal text, nom_municipalite text, longitude double precision, latitude double precision, fiber_available boolean, zone_tarifaire integer, max_speed integer, similarity_score real) LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $function$ DECLARE clean_term text; num_part text; street_part text; BEGIN clean_term := trim(search_term); -- Extract leading number and street part num_part := (regexp_match(clean_term, '^\s*(\d+)'))[1]; street_part := trim(regexp_replace(clean_term, '^\s*\d+\s*', '')); -- Phase 1: If we have a number, filter by numero_municipal prefix first (very fast with btree) -- Then rank by street similarity if street_part exists IF num_part IS NOT NULL AND length(num_part) >= 1 THEN RETURN QUERY SELECT a.identifiant_unique_adresse, a.adresse_formatee, a.numero_municipal, a.numero_unite, a.code_postal, a.odonyme_recompose_normal, a.nom_municipalite, a.longitude, a.latitude, (f.id IS NOT NULL) AS fiber_available, f.zone_tarifaire, f.max_speed, CASE WHEN street_part = '' THEN CASE WHEN a.numero_municipal = num_part THEN 1.0 ELSE 0.5 END::real ELSE similarity(COALESCE(a.odonyme_recompose_normal, ''), street_part) END AS similarity_score FROM public.addresses a LEFT JOIN public.fiber_availability f ON f.uuidadresse = a.identifiant_unique_adresse WHERE a.numero_municipal = num_part AND (street_part = '' OR COALESCE(a.odonyme_recompose_normal, '') % street_part) ORDER BY similarity_score DESC LIMIT result_limit; -- If we got results, return them IF FOUND THEN RETURN; END IF; -- Fallback: prefix match on numero_municipal (e.g. user typed "193") RETURN QUERY SELECT a.identifiant_unique_adresse, a.adresse_formatee, a.numero_municipal, a.numero_unite, a.code_postal, a.odonyme_recompose_normal, a.nom_municipalite, a.longitude, a.latitude, (f.id IS NOT NULL) AS fiber_available, f.zone_tarifaire, f.max_speed, CASE WHEN street_part = '' THEN 0.5::real ELSE similarity(COALESCE(a.odonyme_recompose_normal, ''), street_part) END AS similarity_score FROM public.addresses a LEFT JOIN public.fiber_availability f ON f.uuidadresse = a.identifiant_unique_adresse WHERE a.numero_municipal LIKE (num_part || '%') AND (street_part = '' OR COALESCE(a.odonyme_recompose_normal, '') % street_part) ORDER BY similarity_score DESC, a.numero_municipal LIMIT result_limit; IF FOUND THEN RETURN; END IF; END IF; -- Phase 2: No number found or no results — full trigram search on street/address RETURN QUERY SELECT a.identifiant_unique_adresse, a.adresse_formatee, a.numero_municipal, a.numero_unite, a.code_postal, a.odonyme_recompose_normal, a.nom_municipalite, a.longitude, a.latitude, (f.id IS NOT NULL) AS fiber_available, f.zone_tarifaire, f.max_speed, GREATEST( similarity(COALESCE(a.odonyme_recompose_normal, ''), clean_term), similarity(COALESCE(a.adresse_formatee, ''), clean_term) ) AS similarity_score FROM public.addresses a LEFT JOIN public.fiber_availability f ON f.uuidadresse = a.identifiant_unique_adresse WHERE COALESCE(a.odonyme_recompose_normal, '') % clean_term OR a.adresse_formatee % clean_term ORDER BY similarity_score DESC LIMIT result_limit; END; $function$; -- Btree index on numero_municipal for fast exact/prefix matching CREATE INDEX IF NOT EXISTS idx_addresses_numero_municipal ON public.addresses (numero_municipal);