/** * L13 Planung — geocode sites that have no (or outdated) coordinates, respecting the OSM Nominatim * usage policy: strictly sequential, max. 1 request per second (process-wide limiter in the * provider), identifying User-Agent, results cached on the site (unchanged addresses are skipped). * Manually entered coordinates are never overwritten. * * Usage: * npx tsx scripts/geocode-backfill.ts # all tenants * npx tsx scripts/geocode-backfill.ts --tenant=demo --limit=50 * npx tsx scripts/geocode-backfill.ts --dry-run # only list what would be geocoded * npx tsx scripts/geocode-backfill.ts --retry-not-found # also retry addresses marked not_found * * For larger data sets use a self-hosted or contracted geocoding service (GEOCODING_URL). */ import "dotenv/config"; import { isValidLatLng } from "../src/lib/geo/distance"; import { dbForTenant, prisma } from "../src/server/db"; import { geocodingConfig } from "../src/server/services/geo/config"; import { geocodeSite, type GeocodeOutcome } from "../src/server/services/geo/geocode-site"; import { geocodeQueryFor } from "../src/server/services/geo/normalize"; const arg = (name: string) => process.argv.find((a) => a.startsWith(`--${name}=`))?.split("=")[1]; const flag = (name: string) => process.argv.includes(`--${name}`); async function main() { const cfg = geocodingConfig(); const dryRun = flag("dry-run"); const retryNotFound = flag("retry-not-found"); const limit = Number(arg("limit") ?? Infinity); const slug = arg("tenant"); if (cfg.provider === "none" && !dryRun) { console.log("GEOCODING_PROVIDER=none — nichts zu tun."); return; } console.log(`Provider ${cfg.provider} (${cfg.url}), User-Agent "${cfg.userAgent}", ≤ 1 Anfrage/s${dryRun ? " — Probelauf" : ""}`); const tenants = await prisma.tenant.findMany({ where: slug ? { slug } : {}, select: { id: true, slug: true }, orderBy: { slug: "asc" } }); const totals: Record = {}; let done = 0; for (const tenant of tenants) { const db = dbForTenant(tenant.id); const sites = await db.site.findMany({ where: { deletedAt: null }, select: { id: true, name: true, street: true, houseNumber: true, postalCode: true, city: true, country: true, latitude: true, longitude: true, geocodedAt: true, geocodeStatus: true, geocodeQuery: true }, orderBy: { createdAt: "asc" }, }); const pending = sites.filter((s) => { const query = geocodeQueryFor(s); if (!query) return false; if (isValidLatLng(s) && !s.geocodedAt) return false; // manual coordinates if (s.geocodeQuery !== query) return true; if (s.geocodeStatus === "ok") return false; if (s.geocodeStatus === "not_found") return retryNotFound; return true; }); if (!pending.length) continue; console.log(`\n${tenant.slug}: ${pending.length} Objekt(e)`); for (const site of pending) { if (done >= limit) break; done++; if (dryRun) { console.log(` · ${site.name} (${[site.postalCode, site.city].filter(Boolean).join(" ")})`); continue; } if (retryNotFound && site.geocodeStatus === "not_found") await db.site.update({ where: { id: site.id }, data: { geocodeStatus: null } }); const outcome: GeocodeOutcome = await geocodeSite(tenant.id, site.id); totals[outcome] = (totals[outcome] ?? 0) + 1; console.log(` ${outcome.padEnd(9)} ${site.name}`); } } console.log(`\nFertig: ${done} Objekt(e) ${JSON.stringify(totals)}`); } main() .catch((err) => { console.error(err); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });