L13 Planung: Geocoding der Objekte über OpenStreetMap Nominatim
Provider-Interface (nominatim|none), strukturierte Suche mit User-Agent und Accept-Language, Drosselung 1/s je Prozess, Job geocode-site (Worker: Concurrency 1 + Limiter), Cache am Objekt, manuelle Koordinaten bleiben, Auslöser Anlage/Adressänderung/Import-Bestätigung nur per Queue, Backfill-Skript, Env-Beispiele. Tests mit Fake-Provider, Nominatim nie aufgerufen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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<string, number> = {};
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user