feat(geoloc): OSM geocoder + postal-code centroid fallback (never wrong-city)
The ingest geocode fallback called Mapbox with only a broad territory bbox guard — so a wrong-city guess (e.g. "Rue Elsie" → Valleyfield, both in-territory) was accepted. Rebuilt the chain to be bounded: infra (fibre/placemarks) → RQA exact → OSM (validated) → postal-code centroid → phone-match / town-centre (existing last resorts) - geocodeNominatim(): free OpenStreetMap geocoder (User-Agent per OSM policy, cached, in-territory) — replaces Mapbox in the ingest path. - OSM result is ACCEPTED only if within 10 km of the postal-code centroid (geocodeCentroid, RQA average for the full postal code); otherwise rejected → the centroid is used. So a J0S1H0 address can never land in Valleyfield again. - postal_centroid is the guaranteed bounded fallback. Note on connectors: the fibre table is a PERMANENT direct MySQL link (cfg.LEGACY_DB); placemarks come via the permanent PHP bridge ops_placemarks.php — both already wired into resolveDevCoords. Verified: J0S1H0 centroid = 45.0857,-74.1795 (Huntingdon, 3471 addrs); OSM reachable and returns in-town coords for known streets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e9d1990235
commit
a7a6baf8b5
|
|
@ -227,6 +227,22 @@ async function persistGeocodeToSL (accountId, lat, lon, city) {
|
||||||
// bornes coord()). Cache module. Désactivé si MAPBOX_TOKEN absent de l'env.
|
// bornes coord()). Cache module. Désactivé si MAPBOX_TOKEN absent de l'env.
|
||||||
const MAPBOX_TOKEN = process.env.MAPBOX_TOKEN || ''
|
const MAPBOX_TOKEN = process.env.MAPBOX_TOKEN || ''
|
||||||
const _mbCache = new Map()
|
const _mbCache = new Map()
|
||||||
|
// Géocodeur OSM / Nominatim (GRATUIT — remplace Mapbox dans le repli d'ingest). Politique OSM : User-Agent requis, volume faible
|
||||||
|
// (seuls les jobs non résolus par l'infra/RQA y arrivent), caché. Borné au territoire ; la validation fine (proximité code postal) se fait à l'appel.
|
||||||
|
const _osmCache = new Map()
|
||||||
|
async function geocodeNominatim (addressLine, city, postalCode) {
|
||||||
|
if (!addressLine) return null
|
||||||
|
const key = norm([addressLine, city, postalCode].filter(Boolean).join('|'))
|
||||||
|
if (_osmCache.has(key)) return _osmCache.get(key)
|
||||||
|
let res = null
|
||||||
|
try {
|
||||||
|
const q = [addressLine, city, 'Québec', 'Canada'].filter(Boolean).join(', ')
|
||||||
|
const r = await httpRequest('https://nominatim.openstreetmap.org', '/search?format=json&limit=1&countrycodes=ca&q=' + encodeURIComponent(q), { headers: { 'User-Agent': 'TargoFSM/1.0 (dispatch geocode; louis@targo.ca)', 'Accept-Language': 'fr-CA' }, timeout: 12000 })
|
||||||
|
const f = Array.isArray(r && r.data) && r.data[0]
|
||||||
|
if (f && f.lat && f.lon) { const c = coord(+f.lat, +f.lon); if (c && inTerritory(c.lat, c.lon)) res = c }
|
||||||
|
} catch (e) { log('geocodeNominatim error:', e.message) }
|
||||||
|
_osmCache.set(key, res); return res
|
||||||
|
}
|
||||||
async function geocodeMapbox (addressLine, city, postalCode) {
|
async function geocodeMapbox (addressLine, city, postalCode) {
|
||||||
if (!MAPBOX_TOKEN || !addressLine) return null
|
if (!MAPBOX_TOKEN || !addressLine) return null
|
||||||
const key = norm([addressLine, city, postalCode].filter(Boolean).join('|'))
|
const key = norm([addressLine, city, postalCode].filter(Boolean).join('|'))
|
||||||
|
|
@ -431,14 +447,21 @@ async function buildJob (t) {
|
||||||
payload.service_location = sl.name
|
payload.service_location = sl.name
|
||||||
if (!coordSrc) { const sc = coord(sl.latitude, sl.longitude); if (sc) { payload.latitude = sc.lat; payload.longitude = sc.lon; coordSrc = 'service_location' } } // repli si rien d'autre
|
if (!coordSrc) { const sc = coord(sl.latitude, sl.longitude); if (sc) { payload.latitude = sc.lat; payload.longitude = sc.lon; coordSrc = 'service_location' } } // repli si rien d'autre
|
||||||
}
|
}
|
||||||
if (!coordSrc && addr) { // replis géocodage sur l'adresse de service (sinon facturation) : RQA (autoritaire) puis Mapbox (couverture)
|
if (!coordSrc && addr) { // replis géocodage sur l'adresse de service (sinon facturation) : RQA → OSM (validé) → CENTRE DU CODE POSTAL (borné)
|
||||||
const useSvc = !!svcAddr
|
const useSvc = !!svcAddr
|
||||||
const line = useSvc ? t.dv_addr : t.address1
|
const line = useSvc ? t.dv_addr : t.address1
|
||||||
const zip = useSvc ? t.dv_zip : t.zip
|
const zip = useSvc ? t.dv_zip : t.zip
|
||||||
const ci = useSvc ? t.dv_city : t.city
|
const ci = useSvc ? t.dv_city : t.city
|
||||||
const g = await geocodeRQA(line, zip, ci)
|
const g = await geocodeRQA(line, zip, ci) // 1) RQA exact (autoritaire)
|
||||||
if (g) { payload.latitude = g.lat; payload.longitude = g.lon; coordSrc = 'rqa_geocode' }
|
if (g) { payload.latitude = g.lat; payload.longitude = g.lon; coordSrc = 'rqa_geocode' }
|
||||||
else { const mb = await geocodeMapbox(line, ci, zip); if (mb) { payload.latitude = mb.lat; payload.longitude = mb.lon; coordSrc = 'mapbox_geocode' } }
|
else {
|
||||||
|
// Centre du code postal (moyenne RQA de la zone) = repli BORNÉ + garde-fou : un géocodeur externe qui tombe dans la MAUVAISE
|
||||||
|
// ville (ex. « Rue Elsie » → Valleyfield) est rejeté s'il est à > 10 km du centre du code postal.
|
||||||
|
const ctr = await geocodeCentroid(zip, ci)
|
||||||
|
const osm = await geocodeNominatim(line, ci, zip) // 2) OSM/OpenStreetMap (rue connue absente du RQA)
|
||||||
|
if (osm && (!ctr || _haversineM(osm.lat, osm.lon, ctr.lat, ctr.lon) <= 10000)) { payload.latitude = osm.lat; payload.longitude = osm.lon; coordSrc = 'osm_geocode' }
|
||||||
|
else if (ctr) { payload.latitude = ctr.lat; payload.longitude = ctr.lon; coordSrc = 'postal_centroid' } // 3) repli : jamais la mauvaise ville
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!coordSrc) { // sans compte : retrouver le client par le TÉLÉPHONE du sujet (compte UNIQUE + 1 seule adresse) → adresse PRÉCISE + lien client
|
if (!coordSrc) { // sans compte : retrouver le client par le TÉLÉPHONE du sujet (compte UNIQUE + 1 seule adresse) → adresse PRÉCISE + lien client
|
||||||
const pm = await accountByPhone(pool(), t.subject)
|
const pm = await accountByPhone(pool(), t.subject)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user