gigafibre-fsm/services/targo-hub/lib/address-search.js
louispaulb b6831a1e48 Pont legacy : géocodage RQA via la recherche TRIGRAM (RPC search_addresses) + garde-fou anti-faux-positif
- address-search.js : expose searchAddressesRpc() → RPC Postgres `search_addresses` (pg_trgm), la MÊME
  recherche que l'autocomplete de disponibilité fibre. Trouve les rues que l'ilike manquait (générique géré
  par la colonne odonyme_recompose_long + phase 2 trigram), priorise les CP J0L/J0S (territoire).
- geocodeRQA() (bridge) bascule de l'ilike vers la RPC. Garde-fou : la phase 2 trigram dérive quand le
  civique est absent du RQA (« 2245 René-Vinet » → « Rue Grenet, Montréal »). On n'accepte un résultat que si
  le civique concorde + un token de nom de rue correspond + (territoire J0L/J0S OU CP/ville legacy concordants).
  Vérifié sur les données réelles : accepte 494 Av Curry / 3055 Routhier / 228 Principale / 61 Jean-François ;
  rejette René-Vinet→Grenet/Panet, chemin Ridge→Ferme, rue West→Perras (bons faux positifs écartés).
- Le faible compte RQA (8) = haute précision (l'ilike comptait 17 dont des faux positifs). Mapbox couvre le
  reste (rues neuves/civiques absents) ; ~109/125 (87 %) coordonnés ; les « aucune » = campings/villes mal écrites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:05:14 -04:00

66 lines
3.0 KiB
JavaScript

'use strict'
const { httpRequest } = require('./helpers')
const SUPABASE_URL = 'https://rddrjzptzhypltuzmere.supabase.co'
const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJkZHJqenB0emh5cGx0dXptZXJlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA4MTY4NTYsImV4cCI6MjA4NjM5Mjg1Nn0.EluFlKBze8BYM6AFx88G7kt21EvR18EI3uw1zgCXVzs'
function wordsToIlike (str) {
const words = str.split(/\s+/).filter(w => w.length >= 2)
if (!words.length) return ''
return '*' + words.map(w => encodeURIComponent(w)).join('*') + '*'
}
async function searchAddresses (term, limit = 8) {
const clean = term.trim()
if (clean.length < 3) return []
const numMatch = clean.match(/^\s*(\d+)\s*(.*)/)
const headers = { apikey: SUPABASE_KEY, Authorization: 'Bearer ' + SUPABASE_KEY }
const select = 'adresse_formatee,numero_municipal,numero_unite,code_postal,odonyme_recompose_normal,nom_municipalite,latitude,longitude,identifiant_unique_adresse'
const base = `${SUPABASE_URL}/rest/v1/addresses?select=${select}&limit=${limit}`
let results = []
if (numMatch) {
const num = numMatch[1]
const street = numMatch[2].trim()
let url = `${base}&numero_municipal=eq.${num}`
if (street) url += `&odonyme_recompose_normal=ilike.${wordsToIlike(street)}`
url += '&order=nom_municipalite'
const res = await httpRequest(url, '', { headers })
results = Array.isArray(res.data) ? res.data : []
if (!results.length && num.length >= 2) {
let url2 = `${base}&numero_municipal=like.${num}*`
if (street) url2 += `&odonyme_recompose_normal=ilike.${wordsToIlike(street)}`
url2 += '&order=nom_municipalite'
const res2 = await httpRequest(url2, '', { headers })
results = Array.isArray(res2.data) ? res2.data : []
}
} else {
const pattern = wordsToIlike(clean)
if (!pattern) return []
const url = `${base}&odonyme_recompose_normal=ilike.${pattern}&order=nom_municipalite`
const res = await httpRequest(url, '', { headers })
results = Array.isArray(res.data) ? res.data : []
}
return results.map(a => ({ ...a, fiber_available: false }))
}
// RECHERCHE TRIGRAM (RPC Postgres `search_addresses`) — bien plus robuste que l'ilike ci-dessus :
// phase 1 = numéro civique + mots de rue (sur odonyme normal/court/LONG[avec générique]/municipalité/CP),
// phase 2 = trigram complet (`%`, pg_trgm). Priorise les CP J0L/J0S (territoire). Renvoie aussi
// fiber_available/zone_tarifaire/max_speed/similarity_score. C'est ce qui propulse l'autocomplete de dispo.
async function searchAddressesRpc (term, limit = 8) {
const clean = (term || '').trim()
if (clean.length < 3) return []
const headers = { apikey: SUPABASE_KEY, Authorization: 'Bearer ' + SUPABASE_KEY, 'Content-Type': 'application/json' }
const res = await httpRequest(`${SUPABASE_URL}/rest/v1/rpc/search_addresses`, '', {
method: 'POST', body: JSON.stringify({ search_term: clean, result_limit: limit }), headers,
})
return Array.isArray(res.data) ? res.data : []
}
module.exports = { searchAddresses, searchAddressesRpc, wordsToIlike }