feat(geocoder): seuil de confiance strict, dedup, purge ancien pipeline
Durcissement fiabilité (suite audit).
Confiance & granularité
- worker: un résultat n'est accepté ('done') que si confiance >= GEOCODE_MIN_CONFIDENCE
(0.7) ET granularité non-grossière (rejette country/state/county/region). Sinon
on continue les fallbacks, puis -> llm_needed.
- fix majeur: sur cache hit la confiance était écrite 0.5 en dur (97% des lignes
faussées). Elle est désormais lue depuis geocode_cache (recalculée du raw).
- migration 012: colonnes confidence+granularity sur geocode_cache (recalcul des
entrées Geoapify depuis le raw), geocode_granularity sur band_locations.
Dedup (7x moins de travail)
- fast-path: un band_location dont le (lieu,pays) est déjà 'done' copie le
résultat sans appel API. Index fonctionnel lower(location_raw).
Ancien pipeline retiré
- geocode_queue n'est plus lu nulle part (endpoints /geocoding/reset-errors et
/requeue-all supprimés, /live et /geocoding et Monitor basculés sur
band_locations, panneau admin "ancien pipeline" retiré).
Boutons reset (onglet Géocodage, zone dangereuse)
- POST /admin/api/locations/reset-all: remet tout en queue + efface coords
- POST /admin/api/geocode-cache/purge-nominatim: purge le cache Nominatim
Divers
- groq_worker: coût calculé par modèle (70B vs 8B) au lieu du tarif 70B fixe
- GEOCODE_MIN_CONFIDENCE ajouté aux deux compose
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2b871584e9
commit
72a52a8d3b
7 changed files with 227 additions and 196 deletions
|
|
@ -995,7 +995,7 @@ function conflictCard(b) {
|
||||||
// Géocodage
|
// Géocodage
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
async function renderGeocoding() {
|
async function renderGeocoding() {
|
||||||
if (!document.getElementById("geo-stats")) {
|
if (!document.getElementById("loc-stats")) {
|
||||||
content().innerHTML = `
|
content().innerHTML = `
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:12px">
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:12px">
|
||||||
<div style="font-size:13px;font-weight:700;color:var(--bone)">Pipeline géocodage</div>
|
<div style="font-size:13px;font-weight:700;color:var(--bone)">Pipeline géocodage</div>
|
||||||
|
|
@ -1004,7 +1004,7 @@ async function renderGeocoding() {
|
||||||
|
|
||||||
<div class="card" style="margin-bottom:16px">
|
<div class="card" style="margin-bottom:16px">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:10px">
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:10px">
|
||||||
<h2 style="margin:0">Nouveau pipeline — band_locations</h2>
|
<h2 style="margin:0">Pipeline band_locations</h2>
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
<button class="btn btn-mini" id="loc-enqueue" style="border-color:rgba(63,174,90,0.5);color:var(--ok)">Lancer l'enqueue</button>
|
<button class="btn btn-mini" id="loc-enqueue" style="border-color:rgba(63,174,90,0.5);color:var(--ok)">Lancer l'enqueue</button>
|
||||||
<button class="btn btn-mini" id="loc-reset-errors">Reset erreurs</button>
|
<button class="btn btn-mini" id="loc-reset-errors">Reset erreurs</button>
|
||||||
|
|
@ -1021,20 +1021,19 @@ async function renderGeocoding() {
|
||||||
<div id="loc-feedback" style="margin-top:8px;font-size:12px"></div>
|
<div id="loc-feedback" style="margin-top:8px;font-size:12px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="margin-bottom:16px">
|
<div class="card" style="margin-bottom:16px;border-color:rgba(198,26,26,0.35)">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:10px">
|
<h2 style="margin:0 0 6px">⚠️ Zone dangereuse — tout recommencer</h2>
|
||||||
<h2 style="margin:0">Ancien pipeline — geocode_queue</h2>
|
<p style="font-size:12px;color:var(--muted);margin:0 0 10px">
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
À utiliser si le géocodage est corrompu. « Reset complet » remet toutes les
|
||||||
<button class="btn btn-mini" id="geo-reset-errors">Reset erreurs</button>
|
localisations en queue et efface leurs coordonnées (les appels seront refaits, cache réutilisé
|
||||||
<button class="btn btn-mini" id="geo-requeue-done">Re-queue tout</button>
|
quand possible). « Purger cache Nominatim » supprime les vieux géocodages de l'ancien pipeline
|
||||||
</div>
|
pour forcer un re-géocodage Geoapify frais.
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-mini" id="loc-reset-all" style="border-color:rgba(198,26,26,0.6);color:var(--err)">🔥 Reset complet du géocodage</button>
|
||||||
|
<button class="btn btn-mini" id="cache-purge-nominatim" style="border-color:rgba(198,26,26,0.6);color:var(--err)">🧹 Purger cache Nominatim</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-stats" id="geo-stats" style="margin-bottom:12px"></div>
|
<div id="danger-feedback" style="margin-top:8px;font-size:12px"></div>
|
||||||
<div style="background:rgba(255,255,255,0.06);border-radius:8px;height:12px;overflow:hidden;margin-bottom:6px">
|
|
||||||
<div id="geo-bar" style="width:0%;height:100%;background:linear-gradient(90deg,#3fae5a,rgba(63,174,90,0.5));transition:width 0.4s ease"></div>
|
|
||||||
</div>
|
|
||||||
<div id="geo-pct" style="font-size:12px;color:var(--muted)">—</div>
|
|
||||||
<div id="geo-feedback" style="margin-top:8px;font-size:12px"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
@ -1082,37 +1081,11 @@ async function renderGeocoding() {
|
||||||
makeLocAction("loc-reset-llm", "loc-feedback", "/admin/api/locations/reset-llm", {}, "Remettre en queue les entrées llm_needed et manual ?");
|
makeLocAction("loc-reset-llm", "loc-feedback", "/admin/api/locations/reset-llm", {}, "Remettre en queue les entrées llm_needed et manual ?");
|
||||||
makeLocAction("loc-requeue-all", "loc-feedback", "/admin/api/locations/requeue-all", { include_done: true }, "Re-queue TOUTES les band_locations (erreurs + LLM + manual + done) ?");
|
makeLocAction("loc-requeue-all", "loc-feedback", "/admin/api/locations/requeue-all", { include_done: true }, "Re-queue TOUTES les band_locations (erreurs + LLM + manual + done) ?");
|
||||||
|
|
||||||
// Ancien pipeline — actions
|
// Zone dangereuse — reset complet & purge cache
|
||||||
document.getElementById("geo-reset-errors").addEventListener("click", async () => {
|
makeLocAction("loc-reset-all", "danger-feedback", "/admin/api/locations/reset-all", {},
|
||||||
const btn = document.getElementById("geo-reset-errors");
|
"⚠️ RESET COMPLET : remettre TOUTES les localisations en queue et effacer leurs coordonnées ? Le worker va tout re-géocoder (cache réutilisé quand possible).");
|
||||||
btn.disabled = true;
|
makeLocAction("cache-purge-nominatim", "danger-feedback", "/admin/api/geocode-cache/purge-nominatim", {},
|
||||||
const fb = document.getElementById("geo-feedback");
|
"⚠️ Supprimer toutes les entrées de cache Nominatim (ancien pipeline) ? Ces lieux seront re-géocodés via Geoapify (coûte des appels API).");
|
||||||
fb.textContent = "Réinitialisation…"; fb.style.color = "var(--muted)";
|
|
||||||
try {
|
|
||||||
const r = await api("/admin/api/geocoding/reset-errors", { method: "POST", body: JSON.stringify({}) });
|
|
||||||
fb.textContent = `✓ ${r.count} entrée(s) remise(s) en queue.`;
|
|
||||||
fb.style.color = "var(--ok)";
|
|
||||||
await loadGeocoding();
|
|
||||||
} catch (e) {
|
|
||||||
fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
|
|
||||||
} finally { btn.disabled = false; }
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("geo-requeue-done").addEventListener("click", async () => {
|
|
||||||
if (!confirm("Réinitialiser TOUTES les entrées geocode_queue (erreurs + fait) ?")) return;
|
|
||||||
const btn = document.getElementById("geo-requeue-done");
|
|
||||||
btn.disabled = true;
|
|
||||||
const fb = document.getElementById("geo-feedback");
|
|
||||||
fb.textContent = "Réinitialisation…"; fb.style.color = "var(--muted)";
|
|
||||||
try {
|
|
||||||
const r = await api("/admin/api/geocoding/requeue-all", { method: "POST", body: JSON.stringify({ include_done: true }) });
|
|
||||||
fb.textContent = `✓ ${r.count} entrée(s) re-queueée(s).`;
|
|
||||||
fb.style.color = "var(--ok)";
|
|
||||||
await loadGeocoding();
|
|
||||||
} catch (e) {
|
|
||||||
fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
|
|
||||||
} finally { btn.disabled = false; }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadGeocoding();
|
await loadGeocoding();
|
||||||
|
|
@ -1169,31 +1142,10 @@ async function loadGeocoding() {
|
||||||
const provTxt = (r.providers || []).map((p) => `${esc(p.provider)}: ${p.n.toLocaleString("fr-FR")}${p.avg_conf != null ? ` (conf~${p.avg_conf})` : ""}`).join(" · ");
|
const provTxt = (r.providers || []).map((p) => `${esc(p.provider)}: ${p.n.toLocaleString("fr-FR")}${p.avg_conf != null ? ` (conf~${p.avg_conf})` : ""}`).join(" · ");
|
||||||
const modelTxt = (r.llm_models || []).map((m) => `${esc(m.model)}: ${m.n} (null ${m.n_null})`).join(" · ");
|
const modelTxt = (r.llm_models || []).map((m) => `${esc(m.model)}: ${m.n} (null ${m.n_null})`).join(" · ");
|
||||||
provEl.innerHTML = `<div><strong>Sources géocodage :</strong> ${provTxt || "—"}</div>` +
|
provEl.innerHTML = `<div><strong>Sources géocodage :</strong> ${provTxt || "—"}</div>` +
|
||||||
(modelTxt ? `<div style="margin-top:4px"><strong>Modèles LLM :</strong> ${modelTxt}</div>` : "");
|
(modelTxt ? `<div style="margin-top:4px"><strong>Modèles LLM :</strong> ${modelTxt}</div>` : "") +
|
||||||
|
`<div style="margin-top:4px"><strong>Cache géocodage :</strong> ${(r.cache_size || 0).toLocaleString("fr-FR")} entrées</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ancien pipeline — geocode_queue
|
|
||||||
const total = r.queue.reduce((s, x) => s + x.n, 0);
|
|
||||||
const done = r.queue.find((x) => x.status === "done")?.n || 0;
|
|
||||||
const queued = r.queue.find((x) => x.status === "queued")?.n || 0;
|
|
||||||
const errored = r.queue.find((x) => x.status === "error")?.n || 0;
|
|
||||||
const processing = r.queue.find((x) => x.status === "processing")?.n || 0;
|
|
||||||
const pct = total ? Math.round((done / total) * 100) : 0;
|
|
||||||
|
|
||||||
const statsEl = document.getElementById("geo-stats");
|
|
||||||
if (statsEl) statsEl.innerHTML = `
|
|
||||||
${statCard("Total queue", total)}
|
|
||||||
${statCard("Géocodés", done, "ok")}
|
|
||||||
${statCard("En attente", queued, queued ? "warn" : "")}
|
|
||||||
${statCard("En cours", processing, processing ? "ok" : "")}
|
|
||||||
${statCard("Erreurs", errored, errored ? "err" : "")}
|
|
||||||
${statCard("Cache Geoapify", r.cache_size)}
|
|
||||||
`;
|
|
||||||
const bar = document.getElementById("geo-bar");
|
|
||||||
if (bar) bar.style.width = pct + "%";
|
|
||||||
const pctEl = document.getElementById("geo-pct");
|
|
||||||
if (pctEl) pctEl.textContent = `${pct}% — ${done.toLocaleString("fr-FR")} / ${total.toLocaleString("fr-FR")}${processing ? ` (${processing} en cours)` : ""}`;
|
|
||||||
|
|
||||||
const recentWrap = document.getElementById("geo-recent-wrap");
|
const recentWrap = document.getElementById("geo-recent-wrap");
|
||||||
if (recentWrap) recentWrap.innerHTML = `<table>
|
if (recentWrap) recentWrap.innerHTML = `<table>
|
||||||
<thead><tr><th>MA ID</th><th>Nom</th><th>Pays</th><th>Provider</th><th>Query</th><th>Géocodé le</th><th>Erreur</th></tr></thead>
|
<thead><tr><th>MA ID</th><th>Nom</th><th>Pays</th><th>Provider</th><th>Query</th><th>Géocodé le</th><th>Erreur</th></tr></thead>
|
||||||
|
|
@ -1210,7 +1162,7 @@ async function loadGeocoding() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>`;
|
</table>`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const pctEl = document.getElementById("geo-pct");
|
const pctEl = document.getElementById("loc-pct");
|
||||||
if (pctEl) pctEl.textContent = `Erreur : ${esc(e.message)}`;
|
if (pctEl) pctEl.textContent = `Erreur : ${esc(e.message)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1330,8 +1282,7 @@ async function renderJobs() {
|
||||||
<p style="font-size:12px;color:var(--muted);margin:0 0 14px">Le géocodeur et le worker Groq tournent en continu. L'enqueue parse les bands en band_locations.</p>
|
<p style="font-size:12px;color:var(--muted);margin:0 0 14px">Le géocodeur et le worker Groq tournent en continu. L'enqueue parse les bands en band_locations.</p>
|
||||||
<div style="display:flex;flex-direction:column;gap:8px">
|
<div style="display:flex;flex-direction:column;gap:8px">
|
||||||
<button class="btn" id="cmd-enqueue" style="border-color:rgba(63,174,90,0.5);color:var(--ok)">▶ Lancer l'enqueue band_locations</button>
|
<button class="btn" id="cmd-enqueue" style="border-color:rgba(63,174,90,0.5);color:var(--ok)">▶ Lancer l'enqueue band_locations</button>
|
||||||
<button class="btn" id="cmd-reset-errors">🔄 Reset erreurs geocode_queue</button>
|
<span style="font-size:11px;color:var(--muted)">Les resets/purges du géocodage sont dans l'onglet <a href="#/geocoding">Géocodage</a>.</span>
|
||||||
<button class="btn" id="cmd-requeue-all" style="border-color:rgba(198,26,26,0.5)">♻️ Re-géocoder tout geocode_queue</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="geo-cmd-feedback" style="margin-top:10px;font-size:12px;color:var(--muted)"></div>
|
<div id="geo-cmd-feedback" style="margin-top:10px;font-size:12px;color:var(--muted)"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1394,39 +1345,6 @@ async function renderJobs() {
|
||||||
} finally { btn.disabled = false; }
|
} finally { btn.disabled = false; }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Geocoder reset errors
|
|
||||||
document.getElementById("cmd-reset-errors").addEventListener("click", async () => {
|
|
||||||
const btn = document.getElementById("cmd-reset-errors");
|
|
||||||
btn.disabled = true;
|
|
||||||
const fb = document.getElementById("geo-cmd-feedback");
|
|
||||||
fb.textContent = "Réinitialisation…"; fb.style.color = "var(--muted)";
|
|
||||||
try {
|
|
||||||
const r = await api("/admin/api/geocoding/reset-errors", { method: "POST", body: JSON.stringify({}) });
|
|
||||||
fb.textContent = `✓ ${r.count} entrée(s) remise(s) en queue.`;
|
|
||||||
fb.style.color = "var(--ok)";
|
|
||||||
await loadCmdLogTail();
|
|
||||||
} catch (e) {
|
|
||||||
fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
|
|
||||||
} finally { btn.disabled = false; }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Geocoder requeue all
|
|
||||||
document.getElementById("cmd-requeue-all").addEventListener("click", async () => {
|
|
||||||
if (!confirm("Réinitialiser TOUTES les entrées (erreurs + déjà géocodées) ? Ceci relance le géocodage depuis zéro.")) return;
|
|
||||||
const btn = document.getElementById("cmd-requeue-all");
|
|
||||||
btn.disabled = true;
|
|
||||||
const fb = document.getElementById("geo-cmd-feedback");
|
|
||||||
fb.textContent = "Réinitialisation…"; fb.style.color = "var(--muted)";
|
|
||||||
try {
|
|
||||||
const r = await api("/admin/api/geocoding/requeue-all", { method: "POST", body: JSON.stringify({ include_done: true }) });
|
|
||||||
fb.textContent = `✓ ${r.count} entrée(s) re-queueée(s).`;
|
|
||||||
fb.style.color = "var(--ok)";
|
|
||||||
await loadCmdLogTail();
|
|
||||||
} catch (e) {
|
|
||||||
fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
|
|
||||||
} finally { btn.disabled = false; }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
document.getElementById("cmd-cleanup").addEventListener("click", async () => {
|
document.getElementById("cmd-cleanup").addEventListener("click", async () => {
|
||||||
const btn = document.getElementById("cmd-cleanup");
|
const btn = document.getElementById("cmd-cleanup");
|
||||||
|
|
|
||||||
25
apps/api/migrations/012_geocode_confidence.sql
Normal file
25
apps/api/migrations/012_geocode_confidence.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
-- 012_geocode_confidence.sql
|
||||||
|
-- Durcissement du géocodage : on rend la confiance et la granularité exploitables.
|
||||||
|
--
|
||||||
|
-- Bug corrigé côté worker : sur un cache hit, la confiance était écrite à 0.5 en
|
||||||
|
-- dur, jetant la vraie valeur présente dans le raw. On ajoute des colonnes
|
||||||
|
-- confidence + granularity à geocode_cache et on les recalcule depuis le raw
|
||||||
|
-- Geoapify (FeatureCollection). Aucune purge ici (la purge Nominatim + le reset
|
||||||
|
-- complet sont des boutons admin, pour maîtriser le budget API).
|
||||||
|
|
||||||
|
ALTER TABLE band_locations
|
||||||
|
ADD COLUMN IF NOT EXISTS geocode_granularity TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE geocode_cache
|
||||||
|
ADD COLUMN IF NOT EXISTS confidence REAL,
|
||||||
|
ADD COLUMN IF NOT EXISTS granularity TEXT;
|
||||||
|
|
||||||
|
-- Recalcul pour les entrées Geoapify (raw = FeatureCollection avec rank.confidence)
|
||||||
|
UPDATE geocode_cache
|
||||||
|
SET confidence = NULLIF(raw->'features'->0->'properties'->'rank'->>'confidence', '')::real,
|
||||||
|
granularity = raw->'features'->0->'properties'->>'result_type'
|
||||||
|
WHERE raw ? 'features'
|
||||||
|
AND confidence IS NULL;
|
||||||
|
|
||||||
|
-- Fast-path de dedup au niveau step : lookup par lieu normalisé
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bl_locraw_lower ON band_locations (lower(location_raw));
|
||||||
|
|
@ -454,10 +454,7 @@ export default async function adminRoutes(fastify, opts) {
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
fastify.get("/admin/api/geocoding", async (req, reply) => {
|
fastify.get("/admin/api/geocoding", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const [queue, cache, recent, locations, llm, providers, llmModels] = await Promise.all([
|
const [cache, recent, locations, llm, providers, llmModels] = await Promise.all([
|
||||||
pool.query(`
|
|
||||||
SELECT status, count(*)::int AS n FROM geocode_queue GROUP BY status ORDER BY n DESC
|
|
||||||
`),
|
|
||||||
pool.query(`SELECT count(*)::int AS n FROM geocode_cache`),
|
pool.query(`SELECT count(*)::int AS n FROM geocode_cache`),
|
||||||
pool.query(`
|
pool.query(`
|
||||||
SELECT b.ma_id, b.name, b.country, b.geocode_provider, b.geocoded_at,
|
SELECT b.ma_id, b.name, b.country, b.geocode_provider, b.geocoded_at,
|
||||||
|
|
@ -494,7 +491,6 @@ export default async function adminRoutes(fastify, opts) {
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
queue: queue.rows,
|
|
||||||
cache_size: cache.rows[0].n,
|
cache_size: cache.rows[0].n,
|
||||||
recent: recent.rows,
|
recent: recent.rows,
|
||||||
locations: locations.rows,
|
locations: locations.rows,
|
||||||
|
|
@ -587,45 +583,48 @@ export default async function adminRoutes(fastify, opts) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Géocodage — actions directes sur la queue
|
// Géocodage — RESET complet & purge cache (pour tout relancer proprement)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
fastify.post("/admin/api/geocoding/reset-errors", async (req, reply) => {
|
// Remet TOUTES les band_locations (hors pays-seul) en 'queued' et efface les
|
||||||
|
// résultats : lat/lon, provider, confiance, granularité, query, erreurs, essais.
|
||||||
|
fastify.post("/admin/api/locations/reset-all", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const r = await pool.query(`
|
const r = await pool.query(`
|
||||||
UPDATE geocode_queue
|
UPDATE band_locations
|
||||||
SET status='queued', next_run_at=now(), updated_at=now()
|
SET geocode_status='queued',
|
||||||
WHERE status='error'
|
lat=NULL, lon=NULL,
|
||||||
|
geocode_provider=NULL, geocode_confidence=NULL, geocode_granularity=NULL,
|
||||||
|
geocode_query=NULL, geocode_error=NULL,
|
||||||
|
geocode_tries_geo=0, geocode_tries_llm=0,
|
||||||
|
geocode_next_at=now(), updated_at=now()
|
||||||
|
WHERE is_country_only = FALSE
|
||||||
`);
|
`);
|
||||||
const count = r.rowCount;
|
const count = r.rowCount;
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
|
`INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
|
||||||
["info", `[admin:${req.adminUsername}] geocoding reset-errors: ${count} entrée(s) remise(s) en queue`]
|
["warning", `[admin:${req.adminUsername}] RESET géocodage complet: ${count} band_locations remises à zéro`]
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
return { ok: true, count };
|
return { ok: true, count };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fastify.log.error(err);
|
fastify.log.error(err);
|
||||||
return reply.code(500).send({ ok: false, error: "Erreur reset-errors" });
|
return reply.code(500).send({ ok: false, error: "Erreur reset-all" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post("/admin/api/geocoding/requeue-all", async (req, reply) => {
|
// Purge les entrées de cache issues de l'ancien pipeline Nominatim (raw avec
|
||||||
|
// place_rank). Force le worker à re-géocoder ces lieux via Geoapify.
|
||||||
|
fastify.post("/admin/api/geocode-cache/purge-nominatim", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const { include_done = false } = req.body || {};
|
const r = await pool.query(`DELETE FROM geocode_cache WHERE raw ? 'place_rank'`);
|
||||||
const statuses = include_done ? ["error", "done"] : ["error"];
|
|
||||||
const r = await pool.query(`
|
|
||||||
UPDATE geocode_queue
|
|
||||||
SET status='queued', next_run_at=now(), tries=0, last_error=NULL, updated_at=now()
|
|
||||||
WHERE status = ANY($1::text[])
|
|
||||||
`, [statuses]);
|
|
||||||
const count = r.rowCount;
|
const count = r.rowCount;
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
|
`INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
|
||||||
["info", `[admin:${req.adminUsername}] geocoding requeue-all (include_done=${include_done}): ${count} re-queueée(s)`]
|
["warning", `[admin:${req.adminUsername}] purge cache Nominatim: ${count} entrée(s) supprimée(s)`]
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
return { ok: true, count };
|
return { ok: true, count };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fastify.log.error(err);
|
fastify.log.error(err);
|
||||||
return reply.code(500).send({ ok: false, error: "Erreur requeue-all" });
|
return reply.code(500).send({ ok: false, error: "Erreur purge-nominatim" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -712,11 +711,12 @@ export default async function adminRoutes(fastify, opts) {
|
||||||
SELECT id, job_type, status, requested_by, created_at
|
SELECT id, job_type, status, requested_by, created_at
|
||||||
FROM job_triggers WHERE status = 'pending' ORDER BY created_at ASC
|
FROM job_triggers WHERE status = 'pending' ORDER BY created_at ASC
|
||||||
`),
|
`),
|
||||||
pool.query(`SELECT status, count(*)::int AS n FROM geocode_queue GROUP BY status ORDER BY n DESC`),
|
pool.query(`SELECT geocode_status AS status, count(*)::int AS n FROM band_locations GROUP BY geocode_status ORDER BY n DESC`),
|
||||||
pool.query(`
|
pool.query(`
|
||||||
SELECT gq.ma_id, b.name, b.country, gq.tries, gq.last_error, gq.updated_at
|
SELECT bl.ma_id, b.name, b.country, bl.location_raw, bl.geocode_tries_geo AS tries,
|
||||||
FROM geocode_queue gq JOIN bands b ON b.ma_id = gq.ma_id
|
bl.geocode_error AS last_error, bl.updated_at
|
||||||
WHERE gq.status = 'processing' LIMIT 3
|
FROM band_locations bl JOIN bands b ON b.ma_id = bl.ma_id
|
||||||
|
WHERE bl.geocode_status = 'processing' LIMIT 3
|
||||||
`),
|
`),
|
||||||
pool.query(`SELECT key, value, updated_at FROM crawl_checkpoint ORDER BY key`),
|
pool.query(`SELECT key, value, updated_at FROM crawl_checkpoint ORDER BY key`),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,17 @@ MODELS = [
|
||||||
("llama-3.1-8b-instant", 30, 14400),
|
("llama-3.1-8b-instant", 30, 14400),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Tarifs Groq approximatifs par modèle : (usd/token_in, usd/token_out)
|
||||||
|
PRICING = {
|
||||||
|
"llama-3.3-70b-versatile": (0.59e-6, 0.79e-6),
|
||||||
|
"llama-3.1-8b-instant": (0.05e-6, 0.08e-6),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _cost(model: str, tok_in: int, tok_out: int) -> float:
|
||||||
|
pin, pout = PRICING.get(model, (0.59e-6, 0.79e-6))
|
||||||
|
return tok_in * pin + tok_out * pout
|
||||||
|
|
||||||
_SYSTEM = (
|
_SYSTEM = (
|
||||||
"You are a precise location data extraction assistant. "
|
"You are a precise location data extraction assistant. "
|
||||||
"Your only job is to extract a city name and ISO 3166-1 alpha-2 country code "
|
"Your only job is to extract a city name and ISO 3166-1 alpha-2 country code "
|
||||||
|
|
@ -224,8 +235,7 @@ def main():
|
||||||
city = (parsed or {}).get("city")
|
city = (parsed or {}).get("city")
|
||||||
iso2 = (parsed or {}).get("country") or country
|
iso2 = (parsed or {}).get("country") or country
|
||||||
is_null = not city
|
is_null = not city
|
||||||
# Coût approximatif llama 70B
|
cost_usd = _cost(chosen_model, tok_in, tok_out)
|
||||||
cost_usd = (tok_in * 0.00000059 + tok_out * 0.00000079)
|
|
||||||
|
|
||||||
prompt_text = _USER_TMPL.format(
|
prompt_text = _USER_TMPL.format(
|
||||||
country=country or "unknown (European metal band)",
|
country=country or "unknown (European metal band)",
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,16 @@
|
||||||
worker.py — Géocodeur Geoapify pour band_locations.
|
worker.py — Géocodeur Geoapify pour band_locations.
|
||||||
|
|
||||||
Boucle infinie, traite geocode_status='queued' un par un avec SKIP LOCKED.
|
Boucle infinie, traite geocode_status='queued' un par un avec SKIP LOCKED.
|
||||||
Stratégie progressive :
|
Stratégie :
|
||||||
1. Vérifier geocode_cache (requête déjà faite → gratuit)
|
0. Fast-path dedup : si un autre band_location du MÊME (lieu, pays) est déjà
|
||||||
2. Essayer les requêtes fallback du parser (du plus précis au plus vague)
|
'done', on copie son résultat sans appel API.
|
||||||
3. Après MAX_GEO_TRIES échecs → status='llm_needed' pour groq_worker
|
1. Sinon, essayer les requêtes fallback du parser (précis → vague).
|
||||||
4. Succès → mise à jour band_locations + sync bands.lat/lon
|
Pour chaque requête : cache d'abord (geocode_cache), puis Geoapify.
|
||||||
|
2. Un résultat n'est accepté (done) que s'il est FIABLE :
|
||||||
|
confiance >= MIN_CONFIDENCE ET granularité non-grossière.
|
||||||
|
Sinon on continue les fallbacks.
|
||||||
|
3. Après MAX_GEO_TRIES sans résultat fiable → status='llm_needed'.
|
||||||
|
4. Succès → band_locations + sync bands.lat/lon (origine).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
@ -21,10 +26,14 @@ from parser import build_fallback_queries
|
||||||
GEOAPIFY_API_KEY = os.environ.get("GEOAPIFY_API_KEY", "").strip()
|
GEOAPIFY_API_KEY = os.environ.get("GEOAPIFY_API_KEY", "").strip()
|
||||||
GEOAPIFY_BASE = "https://api.geoapify.com/v1/geocode/search"
|
GEOAPIFY_BASE = "https://api.geoapify.com/v1/geocode/search"
|
||||||
|
|
||||||
MIN_DELAY = float(os.environ.get("GEOCODER_MIN_DELAY", "0.22"))
|
MIN_DELAY = float(os.environ.get("GEOCODER_MIN_DELAY", "0.22"))
|
||||||
JITTER = float(os.environ.get("GEOCODER_JITTER", "0.10"))
|
JITTER = float(os.environ.get("GEOCODER_JITTER", "0.10"))
|
||||||
MAX_PER_RUN = int(os.environ.get("GEOCODE_MAX_PER_RUN", "100000"))
|
MAX_PER_RUN = int(os.environ.get("GEOCODE_MAX_PER_RUN", "100000"))
|
||||||
MAX_GEO_TRIES = int(os.environ.get("GEOCODE_MAX_GEO_TRIES", "3"))
|
MAX_GEO_TRIES = int(os.environ.get("GEOCODE_MAX_GEO_TRIES", "3"))
|
||||||
|
MIN_CONFIDENCE = float(os.environ.get("GEOCODE_MIN_CONFIDENCE", "0.7"))
|
||||||
|
|
||||||
|
# Granularités jugées trop grossières pour une carte de villes : on refuse (→ LLM).
|
||||||
|
COARSE_TYPES = {"country", "state", "county", "region", "province", "political"}
|
||||||
|
|
||||||
_last_call = 0.0
|
_last_call = 0.0
|
||||||
|
|
||||||
|
|
@ -38,6 +47,17 @@ def _polite_sleep():
|
||||||
_last_call = time.monotonic()
|
_last_call = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
def is_reliable(confidence, granularity, is_country_only) -> bool:
|
||||||
|
"""Un géocodage est fiable s'il est assez confiant ET assez précis."""
|
||||||
|
if is_country_only:
|
||||||
|
return True # résolu par centroïde à l'enqueue, granularité pays assumée
|
||||||
|
if confidence is None or confidence < MIN_CONFIDENCE:
|
||||||
|
return False
|
||||||
|
if granularity and granularity.lower() in COARSE_TYPES:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def geoapify_search(query: str) -> dict | None:
|
def geoapify_search(query: str) -> dict | None:
|
||||||
if not GEOAPIFY_API_KEY:
|
if not GEOAPIFY_API_KEY:
|
||||||
raise RuntimeError("GEOAPIFY_API_KEY not set")
|
raise RuntimeError("GEOAPIFY_API_KEY not set")
|
||||||
|
|
@ -55,13 +75,15 @@ def geoapify_search(query: str) -> dict | None:
|
||||||
coords = features[0].get("geometry", {}).get("coordinates", [])
|
coords = features[0].get("geometry", {}).get("coordinates", [])
|
||||||
if len(coords) < 2:
|
if len(coords) < 2:
|
||||||
return None
|
return None
|
||||||
rank = props.get("rank") or {}
|
rank = props.get("rank") or {}
|
||||||
confidence = rank.get("confidence", 0.5) if isinstance(rank, dict) else 0.5
|
confidence = rank.get("confidence") if isinstance(rank, dict) else None
|
||||||
|
granularity = props.get("result_type")
|
||||||
return {
|
return {
|
||||||
"lat": props.get("lat") or coords[1],
|
"lat": props.get("lat") or coords[1],
|
||||||
"lon": props.get("lon") or coords[0],
|
"lon": props.get("lon") or coords[0],
|
||||||
"confidence": confidence,
|
"confidence": confidence,
|
||||||
"raw": data,
|
"granularity": granularity,
|
||||||
|
"raw": data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -69,8 +91,7 @@ def sync_bands_primary(cur, ma_id: int):
|
||||||
"""Met à jour bands.lat/lon avec le lieu d'ORIGINE géocodé (step_order le plus bas).
|
"""Met à jour bands.lat/lon avec le lieu d'ORIGINE géocodé (step_order le plus bas).
|
||||||
|
|
||||||
Carte « metal from Europe » : on veut le point de formation du groupe, pas
|
Carte « metal from Europe » : on veut le point de formation du groupe, pas
|
||||||
sa dernière localisation (ex. un groupe de Thessaloniki parti à Boston doit
|
sa dernière localisation.
|
||||||
rester à Thessaloniki, en Europe).
|
|
||||||
"""
|
"""
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -102,17 +123,63 @@ def sync_bands_primary(cur, ma_id: int):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mark_done(cur, loc_id, ma_id, lat, lon, query, provider, confidence, granularity):
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE band_locations
|
||||||
|
SET lat=%s, lon=%s,
|
||||||
|
geocode_status='done',
|
||||||
|
geocode_query=%s,
|
||||||
|
geocode_provider=%s,
|
||||||
|
geocode_confidence=%s,
|
||||||
|
geocode_granularity=%s,
|
||||||
|
geocode_error=NULL,
|
||||||
|
updated_at=now()
|
||||||
|
WHERE id=%s
|
||||||
|
""",
|
||||||
|
(lat, lon, query, provider, confidence, granularity, loc_id),
|
||||||
|
)
|
||||||
|
sync_bands_primary(cur, ma_id)
|
||||||
|
|
||||||
|
|
||||||
|
def try_dedup(cur, loc_id, ma_id, location_raw, country) -> bool:
|
||||||
|
"""Fast-path : copier le résultat d'un band_location identique déjà 'done'."""
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT bl2.lat, bl2.lon, bl2.geocode_confidence, bl2.geocode_granularity,
|
||||||
|
bl2.geocode_query
|
||||||
|
FROM band_locations bl2
|
||||||
|
JOIN bands b2 ON b2.ma_id = bl2.ma_id
|
||||||
|
WHERE lower(bl2.location_raw) = lower(%s)
|
||||||
|
AND COALESCE(b2.country,'') = COALESCE(%s,'')
|
||||||
|
AND bl2.geocode_status = 'done'
|
||||||
|
AND bl2.lat IS NOT NULL
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(location_raw, country),
|
||||||
|
)
|
||||||
|
hit = cur.fetchone()
|
||||||
|
if not hit:
|
||||||
|
return False
|
||||||
|
lat, lon, conf, gran, query = hit
|
||||||
|
mark_done(cur, loc_id, ma_id, lat, lon, query, 'dedup', conf, gran)
|
||||||
|
print(f"[worker] id={loc_id} DEDUP '{location_raw}' ({country})")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
dsn = os.environ["DATABASE_URL"]
|
dsn = os.environ["DATABASE_URL"]
|
||||||
conn = psycopg2.connect(dsn)
|
conn = psycopg2.connect(dsn)
|
||||||
conn.autocommit = True
|
conn.autocommit = True
|
||||||
|
|
||||||
|
print(f"[worker] démarré — seuil confiance={MIN_CONFIDENCE}, max_tries={MAX_GEO_TRIES}")
|
||||||
|
|
||||||
processed = 0
|
processed = 0
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
while processed < MAX_PER_RUN:
|
while processed < MAX_PER_RUN:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT bl.id, bl.ma_id, bl.location_raw,
|
SELECT bl.id, bl.ma_id, bl.location_raw, bl.is_country_only,
|
||||||
bl.geocode_tries_geo, bl.geocode_query, b.country
|
bl.geocode_tries_geo, bl.geocode_query, b.country
|
||||||
FROM band_locations bl
|
FROM band_locations bl
|
||||||
JOIN bands b ON b.ma_id = bl.ma_id
|
JOIN bands b ON b.ma_id = bl.ma_id
|
||||||
|
|
@ -129,21 +196,25 @@ def main():
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
loc_id, ma_id, location_raw, tries, llm_query, country = row
|
loc_id, ma_id, location_raw, is_country_only, tries, llm_query, country = row
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"UPDATE band_locations SET geocode_status='processing', updated_at=now() WHERE id=%s",
|
"UPDATE band_locations SET geocode_status='processing', updated_at=now() WHERE id=%s",
|
||||||
(loc_id,),
|
(loc_id,),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Liste de requêtes à essayer : requête LLM en premier si disponible
|
# 0. Fast-path dedup (aucun appel API)
|
||||||
|
if try_dedup(cur, loc_id, ma_id, location_raw, country):
|
||||||
|
processed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Requêtes à essayer : requête LLM en premier si disponible
|
||||||
fallbacks = build_fallback_queries(location_raw, country)
|
fallbacks = build_fallback_queries(location_raw, country)
|
||||||
if llm_query and llm_query not in fallbacks:
|
if llm_query and llm_query not in fallbacks:
|
||||||
queries = [llm_query] + fallbacks
|
queries = [llm_query] + fallbacks
|
||||||
else:
|
else:
|
||||||
queries = fallbacks
|
queries = fallbacks
|
||||||
|
|
||||||
# Déduplique en gardant l'ordre
|
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
unique: list[str] = []
|
unique: list[str] = []
|
||||||
for q in queries:
|
for q in queries:
|
||||||
|
|
@ -151,53 +222,71 @@ def main():
|
||||||
seen.add(q)
|
seen.add(q)
|
||||||
unique.append(q)
|
unique.append(q)
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
rate_limit = False
|
rate_limit = False
|
||||||
used_query = None
|
used_query = None
|
||||||
lat = lon = None
|
lat = lon = None
|
||||||
confidence = 0.5
|
confidence = None
|
||||||
provider = 'geoapify'
|
granularity = None
|
||||||
last_err = None
|
provider = 'geoapify'
|
||||||
|
last_err = None
|
||||||
|
|
||||||
for query in unique:
|
for query in unique:
|
||||||
# Cache hit ?
|
# Cache hit ? (avec confiance/granularité recalculées)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT lat, lon FROM geocode_cache WHERE query=%s",
|
"SELECT lat, lon, confidence, granularity FROM geocode_cache WHERE query=%s",
|
||||||
(query,),
|
(query,),
|
||||||
)
|
)
|
||||||
cached = cur.fetchone()
|
cached = cur.fetchone()
|
||||||
if cached and cached[0] is not None:
|
if cached and cached[0] is not None:
|
||||||
lat, lon = cached
|
c_lat, c_lon, c_conf, c_gran = cached
|
||||||
used_query = query
|
if is_reliable(c_conf, c_gran, is_country_only):
|
||||||
provider = 'geoapify-cache'
|
lat, lon = c_lat, c_lon
|
||||||
success = True
|
confidence = c_conf
|
||||||
print(f"[worker] id={loc_id} cache HIT '{query}'")
|
granularity = c_gran
|
||||||
break
|
used_query = query
|
||||||
|
provider = 'geoapify-cache'
|
||||||
|
success = True
|
||||||
|
print(f"[worker] id={loc_id} cache HIT '{query}' conf={c_conf}")
|
||||||
|
break
|
||||||
|
# cache présent mais peu fiable → inutile de rappeler l'API pour cette requête
|
||||||
|
last_err = f"low_conf_cache:{c_conf}:'{query}'"
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = geoapify_search(query)
|
res = geoapify_search(query)
|
||||||
if res:
|
if res:
|
||||||
lat = float(res["lat"])
|
r_lat = float(res["lat"])
|
||||||
lon = float(res["lon"])
|
r_lon = float(res["lon"])
|
||||||
confidence = float(res.get("confidence") or 0.5)
|
r_conf = res.get("confidence")
|
||||||
used_query = query
|
r_gran = res.get("granularity")
|
||||||
|
# Toujours mettre en cache (même peu fiable) pour éviter de rappeler
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO geocode_cache
|
INSERT INTO geocode_cache
|
||||||
(query, provider, lat, lon, geom, raw, updated_at)
|
(query, provider, lat, lon, geom, raw, confidence, granularity, updated_at)
|
||||||
VALUES (%s,'geoapify',%s,%s,
|
VALUES (%s,'geoapify',%s,%s,
|
||||||
ST_SetSRID(ST_MakePoint(%s,%s),4326)::geography,
|
ST_SetSRID(ST_MakePoint(%s,%s),4326)::geography,
|
||||||
%s, now())
|
%s, %s, %s, now())
|
||||||
ON CONFLICT (query) DO UPDATE
|
ON CONFLICT (query) DO UPDATE
|
||||||
SET lat=EXCLUDED.lat, lon=EXCLUDED.lon,
|
SET lat=EXCLUDED.lat, lon=EXCLUDED.lon,
|
||||||
geom=EXCLUDED.geom, raw=EXCLUDED.raw,
|
geom=EXCLUDED.geom, raw=EXCLUDED.raw,
|
||||||
|
confidence=EXCLUDED.confidence,
|
||||||
|
granularity=EXCLUDED.granularity,
|
||||||
provider=EXCLUDED.provider, updated_at=now()
|
provider=EXCLUDED.provider, updated_at=now()
|
||||||
""",
|
""",
|
||||||
(query, lat, lon, lon, lat, json.dumps(res["raw"])),
|
(query, r_lat, r_lon, r_lon, r_lat,
|
||||||
|
json.dumps(res["raw"]), r_conf, r_gran),
|
||||||
)
|
)
|
||||||
success = True
|
if is_reliable(r_conf, r_gran, is_country_only):
|
||||||
print(f"[worker] id={loc_id} OK '{query}' lat={lat:.4f} lon={lon:.4f}")
|
lat, lon = r_lat, r_lon
|
||||||
break
|
confidence = r_conf
|
||||||
|
granularity = r_gran
|
||||||
|
used_query = query
|
||||||
|
success = True
|
||||||
|
print(f"[worker] id={loc_id} OK '{query}' conf={r_conf} gran={r_gran}")
|
||||||
|
break
|
||||||
|
last_err = f"low_conf:{r_conf}:{r_gran}:'{query}'"
|
||||||
else:
|
else:
|
||||||
last_err = f"no_result:'{query}'"
|
last_err = f"no_result:'{query}'"
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
|
|
@ -224,21 +313,8 @@ def main():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
cur.execute(
|
mark_done(cur, loc_id, ma_id, lat, lon, used_query,
|
||||||
"""
|
provider, confidence, granularity)
|
||||||
UPDATE band_locations
|
|
||||||
SET lat=%s, lon=%s,
|
|
||||||
geocode_status='done',
|
|
||||||
geocode_query=%s,
|
|
||||||
geocode_provider=%s,
|
|
||||||
geocode_confidence=%s,
|
|
||||||
geocode_error=NULL,
|
|
||||||
updated_at=now()
|
|
||||||
WHERE id=%s
|
|
||||||
""",
|
|
||||||
(lat, lon, used_query, provider, confidence, loc_id),
|
|
||||||
)
|
|
||||||
sync_bands_primary(cur, ma_id)
|
|
||||||
else:
|
else:
|
||||||
new_tries = tries + 1
|
new_tries = tries + 1
|
||||||
if new_tries >= MAX_GEO_TRIES:
|
if new_tries >= MAX_GEO_TRIES:
|
||||||
|
|
@ -251,9 +327,9 @@ def main():
|
||||||
updated_at=now()
|
updated_at=now()
|
||||||
WHERE id=%s
|
WHERE id=%s
|
||||||
""",
|
""",
|
||||||
(new_tries, last_err or "all_fallbacks_failed", loc_id),
|
(new_tries, last_err or "no_reliable_match", loc_id),
|
||||||
)
|
)
|
||||||
print(f"[worker] id={loc_id} → llm_needed après {new_tries} essais")
|
print(f"[worker] id={loc_id} → llm_needed après {new_tries} essais ({last_err})")
|
||||||
else:
|
else:
|
||||||
backoff = min(1440, 30 * (2 ** new_tries))
|
backoff = min(1440, 30 * (2 ** new_tries))
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ services:
|
||||||
GEOCODER_JITTER: "0.10"
|
GEOCODER_JITTER: "0.10"
|
||||||
GEOCODE_MAX_PER_RUN: "100000"
|
GEOCODE_MAX_PER_RUN: "100000"
|
||||||
GEOCODE_MAX_GEO_TRIES: "3"
|
GEOCODE_MAX_GEO_TRIES: "3"
|
||||||
|
GEOCODE_MIN_CONFIDENCE: "0.7"
|
||||||
command: ["python", "src/worker.py"]
|
command: ["python", "src/worker.py"]
|
||||||
networks:
|
networks:
|
||||||
- coolify
|
- coolify
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ services:
|
||||||
GEOCODER_JITTER: "0.10"
|
GEOCODER_JITTER: "0.10"
|
||||||
GEOCODE_MAX_PER_RUN: "100000"
|
GEOCODE_MAX_PER_RUN: "100000"
|
||||||
GEOCODE_MAX_GEO_TRIES: "3"
|
GEOCODE_MAX_GEO_TRIES: "3"
|
||||||
|
GEOCODE_MIN_CONFIDENCE: "0.7"
|
||||||
command: ["python", "src/worker.py"]
|
command: ["python", "src/worker.py"]
|
||||||
networks:
|
networks:
|
||||||
- coolify
|
- coolify
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue