From 2b9a863d3992ea827dbbf96cd4a361edb3a20658 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sun, 7 Jun 2026 17:14:41 -0400 Subject: [PATCH] =?UTF-8?q?Conformit=C3=A9=20:=20repli=20=C2=AB=20centre?= =?UTF-8?q?=20du=20code=20postal=20/=20ville=20=C2=BB=20pour=20les=20unmat?= =?UTF-8?q?ched=20restants=20(statut=20'area')?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dernier recours quand l'adresse exacte est introuvable : placer le Service Location au CENTROÏDE (rqa_addresses) de son code postal (préféré) sinon de sa ville → le job apparaît dans le bon secteur. - Hub : applyAreaFallback() (CTE centroïdes CP/ville, index-friendly) + POST /address/conformity/apply-area. Statut 'area', linked_address '≈ centre '. Hors-QC/junk (absents de rqa_addresses) restent unmatched. - Ops : carte stat « ≈ Secteur (CP/ville) » + bouton « ≈ Centre CP/ville (reste) » dans la page Conformité. Exécuté : 317 placés (303 par code postal, 14 par ville) → unmatched 365 → 48 (Toronto/boîtes postales/junk). État final : validated 16 257 · review 489 · area 317 · unmatched 48 → 99,7 % des services ont une position. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/api/address.js | 2 ++ apps/ops/src/pages/AddressConformityPage.vue | 15 +++++++++- services/targo-hub/lib/address-conformity.js | 31 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/apps/ops/src/api/address.js b/apps/ops/src/api/address.js index 7bc28b3..60f4d1c 100644 --- a/apps/ops/src/api/address.js +++ b/apps/ops/src/api/address.js @@ -25,3 +25,5 @@ export const conformityResolve = (body) => jpost('/address/conformity/resolve', export const campingsList = () => jget('/address/conformity/campings') export const campingsUpsert = (body) => jpost('/address/conformity/campings', body) // {keyword,name,address,latitude,longitude} export const campingsApply = () => jpost('/address/conformity/campings/apply', {}) +// Dernier repli : placer les unmatched restants au centre du code postal (sinon ville) → statut 'area' +export const conformityApplyArea = () => jpost('/address/conformity/apply-area', {}) diff --git a/apps/ops/src/pages/AddressConformityPage.vue b/apps/ops/src/pages/AddressConformityPage.vue index c87ad3b..b5dab33 100644 --- a/apps/ops/src/pages/AddressConformityPage.vue +++ b/apps/ops/src/pages/AddressConformityPage.vue @@ -3,6 +3,7 @@
Conformité des adresses
+ Placer les adresses non matchées restantes au centre de leur code postal (sinon ville) — dernier repli Rafraîchir
@@ -171,8 +172,9 @@ const typeN = (k) => { const x = stats.by_type.find(s => s.t === k); return x ? const statusCards = computed(() => [ { key: 'validated', label: 'Validées (conformes)', n: statN('validated'), cls: 'text-positive' }, { key: 'review', label: 'À confirmer', n: statN('review'), cls: 'text-amber-8' }, + { key: 'area', label: '≈ Secteur (CP/ville)', n: statN('area'), cls: 'text-blue-grey-7' }, { key: 'unmatched', label: 'Non matchées', n: statN('unmatched'), cls: 'text-deep-orange' }, - { key: 'no_address', label: 'Sans adresse (rejetées)', n: statN('no_address'), cls: 'text-grey-7' }, + { key: 'no_address', label: 'Rejetées (sans adresse)', n: statN('no_address'), cls: 'text-grey-7' }, ]) const typeChips = computed(() => [ { key: '', label: 'Tout', n: statN('review') + statN('unmatched') }, @@ -254,5 +256,16 @@ async function applyCampings () { catch (e) { $q.notify({ type: 'negative', message: e.message }) } finally { campSaving.value = false } } +// ── Dernier repli : centre du code postal / ville pour les unmatched restants ── +const areaSaving = ref(false) +async function applyArea () { + areaSaving.value = true + try { + const r = await addr.conformityApplyArea() + $q.notify({ type: 'positive', message: `${r.total || 0} placés au secteur (${r.by_postal || 0} par code postal, ${r.by_city || 0} par ville)`, timeout: 2600 }) + reload() + } catch (e) { $q.notify({ type: 'negative', message: 'Échec: ' + e.message }) } finally { areaSaving.value = false } +} + onMounted(() => { loadStats(); loadList(); loadCampings() }) diff --git a/services/targo-hub/lib/address-conformity.js b/services/targo-hub/lib/address-conformity.js index edfc81d..b515ecb 100644 --- a/services/targo-hub/lib/address-conformity.js +++ b/services/targo-hub/lib/address-conformity.js @@ -30,6 +30,33 @@ async function applyCampings (p) { return r.rows } +// Dernier repli : pour les 'unmatched' qu'on n'a pas pu géocoder, placer au CENTRE (centroïde rqa_addresses) +// du code postal (préféré, plus précis) sinon de la ville → le job apparaît au moins dans le bon secteur. +// Statut 'area' (approximatif). Les hors-QC/junk (pas dans rqa_addresses) restent 'unmatched'. +async function applyAreaFallback (p) { + const r = await p.query(` + WITH um AS ( + SELECT name, replace(upper(coalesce(postal_code,'')),' ','') cp, lower(unaccent(coalesce(city,''))) city, + postal_code cp_raw, city city_raw + FROM "tabService Location" WHERE address_validation_status='unmatched' + ), + pc AS (SELECT replace(upper(code_postal),' ','') cp, avg(latitude) lat, avg(longitude) lon FROM rqa_addresses + WHERE replace(upper(code_postal),' ','') IN (SELECT cp FROM um WHERE cp ~ '^[A-Z][0-9][A-Z][0-9]') GROUP BY 1), + cc AS (SELECT lower(unaccent(ville)) city, avg(latitude) lat, avg(longitude) lon FROM rqa_addresses + WHERE lower(unaccent(ville)) IN (SELECT city FROM um WHERE city NOT IN ('','n/a','ville','x','-')) GROUP BY 1), + upd AS ( + UPDATE "tabService Location" sl SET + latitude=COALESCE(pc.lat,cc.lat), longitude=COALESCE(pc.lon,cc.lon), + linked_address='≈ centre ' || COALESCE(NULLIF(CASE WHEN pc.lat IS NOT NULL THEN um.cp_raw END,''), um.city_raw), + address_validation_status='area', address_validated_at=NOW(), modified=NOW() + FROM um LEFT JOIN pc ON pc.cp=um.cp LEFT JOIN cc ON cc.city=um.city + WHERE sl.name=um.name AND (pc.lat IS NOT NULL OR cc.lat IS NOT NULL) + RETURNING (pc.lat IS NOT NULL) AS by_postal) + SELECT count(*)::int total, count(*) FILTER (WHERE by_postal)::int by_postal, + count(*) FILTER (WHERE NOT by_postal)::int by_city FROM upd`) + return r.rows[0] +} + // Type d'adresse (pour trier la file) : camping (sobriquet de lot), civique à corriger, non-adresse, review standard. const TYPE_SQL = `CASE WHEN sl.city ILIKE '%camping%' OR sl.address_line ILIKE '%camping%' THEN 'camping' @@ -137,6 +164,10 @@ async function handle (req, res, method, path) { if (path === '/address/conformity/campings/apply' && method === 'POST') { cors(res); return json(res, 200, { ok: true, applied: await applyCampings(pool()) }) } + // Repli centroïde (centre du CP/ville) pour les unmatched restants + if (path === '/address/conformity/apply-area' && method === 'POST') { + cors(res); return json(res, 200, { ok: true, ...(await applyAreaFallback(pool())) }) + } cors(res); return json(res, 404, { ok: false, error: 'route conformité inconnue' }) } catch (e) {