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>
82 lines
3.6 KiB
TypeScript
82 lines
3.6 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|