@cenogram/mcp-server 0.5.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -8
- package/dist/api-client.d.ts +279 -1
- package/dist/api-client.js +117 -4
- package/dist/error-messages.js +1 -1
- package/dist/formatters.d.ts +14 -1
- package/dist/formatters.js +647 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +129 -108
- package/dist/tools.js +336 -47
- package/package.json +8 -2
package/dist/formatters.js
CHANGED
|
@@ -254,7 +254,7 @@ export function formatMarketOverview(stats) {
|
|
|
254
254
|
export function formatPriceStats(rows, location) {
|
|
255
255
|
if (rows.length === 0) {
|
|
256
256
|
return location
|
|
257
|
-
? `No price statistics found for "${location}". Note: this endpoint only covers residential units (apartments).
|
|
257
|
+
? `No price statistics found for "${location}". Note: this endpoint only covers residential units (apartments), and matches on name only. Call list_locations(search=...) to confirm the name is a valid RCN district (rcn_district).`
|
|
258
258
|
: "No price statistics available.";
|
|
259
259
|
}
|
|
260
260
|
const header = location
|
|
@@ -291,7 +291,7 @@ export function formatHistogram(bins) {
|
|
|
291
291
|
}
|
|
292
292
|
export function formatParcelResults(res, query) {
|
|
293
293
|
if (res.results.length === 0) {
|
|
294
|
-
return `No parcels found matching "${query}"
|
|
294
|
+
return `No parcels found matching "${query}". This searches the parcels we hold, which are near-complete coverage of the cadastral register rather than the whole of it, so a miss is more likely a recent change or a mistyped prefix than evidence that no such parcel exists — ${CORPUS_COVERAGE_HINT}${formatCorpusCoverage(res.corpus_coverage)}`;
|
|
295
295
|
}
|
|
296
296
|
const lines = [`Found ${res.results.length} parcels matching "${query}":\n`];
|
|
297
297
|
for (const [i, p] of res.results.entries()) {
|
|
@@ -301,11 +301,180 @@ export function formatParcelResults(res, query) {
|
|
|
301
301
|
lines.push(`${i + 1}. ${p.parcel_id ?? "(parcel number requires a paid plan)"}`);
|
|
302
302
|
lines.push(` District: ${district} | Area: ${area} | Location: ${location}`);
|
|
303
303
|
}
|
|
304
|
+
return lines.join("\n") + formatCorpusCoverage(res.corpus_coverage);
|
|
305
|
+
}
|
|
306
|
+
const PARCEL_IDENTITY_WITHHELD = "(parcel number requires a paid plan)";
|
|
307
|
+
const CORPUS_COVERAGE_HINT = "looking the parcel up by its FULL cadastral id (resolve_parcel) goes down a different path that can confirm and add a parcel we do not yet hold.";
|
|
308
|
+
const COVERAGE_UNMEASURED_FALLBACK = "we hold near-complete coverage of the cadastral register, though not the whole of it and not live, " +
|
|
309
|
+
"and how much of it this query covers is not measured. Read the answer above as what we hold, not " +
|
|
310
|
+
"as what is there.";
|
|
311
|
+
function finiteOrNull(value) {
|
|
312
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
313
|
+
}
|
|
314
|
+
function coverageScopeLabel(counties) {
|
|
315
|
+
if (counties === 1)
|
|
316
|
+
return "this county";
|
|
317
|
+
if (counties != null && counties > 1)
|
|
318
|
+
return `these ${counties.toLocaleString("en-US")} counties`;
|
|
319
|
+
return "the counties this query addresses";
|
|
320
|
+
}
|
|
321
|
+
function coveragePctLabel(held, source, reported) {
|
|
322
|
+
const exact = (held / source) * 100;
|
|
323
|
+
if (held > 0 && exact < 0.05)
|
|
324
|
+
return "<0.1%";
|
|
325
|
+
if (held < source && exact >= 99.95)
|
|
326
|
+
return ">99.9%";
|
|
327
|
+
return `${(reported ?? exact).toFixed(1)}%`;
|
|
328
|
+
}
|
|
329
|
+
export function formatCorpusCoverage(cov) {
|
|
330
|
+
if (!cov)
|
|
331
|
+
return "";
|
|
332
|
+
const asOf = typeof cov.as_of === "string" ? cov.as_of.split("T")[0] : null;
|
|
333
|
+
const held = finiteOrNull(cov.held_parcels);
|
|
334
|
+
const source = finiteOrNull(cov.source_parcels);
|
|
335
|
+
if (held != null && source != null && source > 0) {
|
|
336
|
+
if (held >= source)
|
|
337
|
+
return "";
|
|
338
|
+
const scope = coverageScopeLabel(finiteOrNull(cov.counties));
|
|
339
|
+
const pct = coveragePctLabel(held, source, finiteOrNull(cov.held_pct));
|
|
340
|
+
const stamp = asOf ? `, measured ${asOf}` : "";
|
|
341
|
+
return `\n\nCOVERAGE: we hold ${held.toLocaleString("en-US")} of the ${source.toLocaleString("en-US")} parcels the cadastral register lists for ${scope} (${pct}${stamp}). A short or empty list above reflects that, not the land.`;
|
|
342
|
+
}
|
|
343
|
+
const note = typeof cov.note === "string" && cov.note.trim().length > 0
|
|
344
|
+
? cov.note.trim()
|
|
345
|
+
: COVERAGE_UNMEASURED_FALLBACK;
|
|
346
|
+
const stamp = asOf ? ` Our latest coverage measurement is dated ${asOf}.` : "";
|
|
347
|
+
return `\n\nCOVERAGE: ${note}${stamp}`;
|
|
348
|
+
}
|
|
349
|
+
function collectPositions(node, out) {
|
|
350
|
+
if (!Array.isArray(node))
|
|
351
|
+
return;
|
|
352
|
+
if (node.length >= 2 && typeof node[0] === "number" && typeof node[1] === "number") {
|
|
353
|
+
out.push([node[0], node[1]]);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
for (const child of node)
|
|
357
|
+
collectPositions(child, out);
|
|
358
|
+
}
|
|
359
|
+
function outlineCentre(geometry) {
|
|
360
|
+
if (!geometry)
|
|
361
|
+
return null;
|
|
362
|
+
const positions = [];
|
|
363
|
+
collectPositions(geometry.coordinates, positions);
|
|
364
|
+
if (positions.length === 0)
|
|
365
|
+
return null;
|
|
366
|
+
let minLng = Infinity, maxLng = -Infinity, minLat = Infinity, maxLat = -Infinity;
|
|
367
|
+
for (const [lng, lat] of positions) {
|
|
368
|
+
if (lng < minLng)
|
|
369
|
+
minLng = lng;
|
|
370
|
+
if (lng > maxLng)
|
|
371
|
+
maxLng = lng;
|
|
372
|
+
if (lat < minLat)
|
|
373
|
+
minLat = lat;
|
|
374
|
+
if (lat > maxLat)
|
|
375
|
+
maxLat = lat;
|
|
376
|
+
}
|
|
377
|
+
return { lat: (minLat + maxLat) / 2, lng: (minLng + maxLng) / 2 };
|
|
378
|
+
}
|
|
379
|
+
function formatParcelAddress(p) {
|
|
380
|
+
if (p.street == null || p.street === "")
|
|
381
|
+
return null;
|
|
382
|
+
const approx = p.address_source === "approx_high" || p.address_source === "approx_low";
|
|
383
|
+
const tag = approx ? " [street approximate — derived, not from the record]" : "";
|
|
384
|
+
return [p.street, p.building_number].filter(Boolean).join(" ") + tag;
|
|
385
|
+
}
|
|
386
|
+
export function formatParcelList(res, scope, creditsRefunded = false) {
|
|
387
|
+
if (res.data.length === 0) {
|
|
388
|
+
const lines = [
|
|
389
|
+
`No parcels found for ${scope}. The filter is valid; nothing matched it AMONG THE PARCELS WE HOLD, which are near-complete coverage of the cadastral register rather than the whole of it. So this is not evidence that the area has no parcels.`,
|
|
390
|
+
];
|
|
391
|
+
if (creditsRefunded)
|
|
392
|
+
lines.push("The tokens for this call were refunded — an unmatched street page costs nothing.");
|
|
393
|
+
const streets = res.suggestions?.streets ?? [];
|
|
394
|
+
const numbers = res.suggestions?.building_numbers ?? [];
|
|
395
|
+
if (streets.length > 0) {
|
|
396
|
+
lines.push("Close street names on record here — matching is case- and accent-insensitive but does not inflect, so retry with one of these, each a ready next call:");
|
|
397
|
+
for (const s of streets)
|
|
398
|
+
lines.push(` - street="${s}"`);
|
|
399
|
+
}
|
|
400
|
+
if (numbers.length > 0) {
|
|
401
|
+
lines.push("The street is on record but that number is not; these are the numbers held on it, in the compound form the register uses — retry with one of them:");
|
|
402
|
+
for (const n of numbers)
|
|
403
|
+
lines.push(` - buildingNumber="${n}"`);
|
|
404
|
+
}
|
|
405
|
+
if (streets.length === 0 && numbers.length === 0) {
|
|
406
|
+
lines.push("Widening the area or dropping a surface filter is the next step.");
|
|
407
|
+
}
|
|
408
|
+
lines.push(CORPUS_COVERAGE_HINT);
|
|
409
|
+
return lines.join("\n") + formatCorpusCoverage(res.corpus_coverage);
|
|
410
|
+
}
|
|
411
|
+
const lines = [`Found ${res.data.length} parcel${res.data.length === 1 ? "" : "s"} for ${scope}:\n`];
|
|
412
|
+
for (const [i, p] of res.data.entries()) {
|
|
413
|
+
const district = p.district ?? "Unknown";
|
|
414
|
+
const location = p.lat != null && p.lng != null
|
|
415
|
+
? `${p.lat.toFixed(4)}°N, ${p.lng.toFixed(4)}°E`
|
|
416
|
+
: "no outline held";
|
|
417
|
+
lines.push(`${i + 1}. ${p.parcel_id ?? PARCEL_IDENTITY_WITHHELD}`);
|
|
418
|
+
lines.push(` District: ${district} | Location: ${location}`);
|
|
419
|
+
const address = formatParcelAddress(p);
|
|
420
|
+
if (address)
|
|
421
|
+
lines.push(` Address: ${address}`);
|
|
422
|
+
}
|
|
423
|
+
if (res.pagination.has_more && res.pagination.next_cursor) {
|
|
424
|
+
lines.push(`\nMore parcels match than are shown. Pass cursor="${res.pagination.next_cursor}" to get the next page (the value is opaque — pass it back unchanged).`);
|
|
425
|
+
}
|
|
426
|
+
else if (res.pagination.has_more) {
|
|
427
|
+
lines.push(`\nMore parcels match than are shown, but no cursor came back — narrow the filter instead of paging.`);
|
|
428
|
+
}
|
|
429
|
+
lines.push(`\nNo outline and no surface on these rows: call list_parcels_in_area with includeGeometry=true (or a polygon) for outlines, or get_parcel_report for one parcel in full.`);
|
|
430
|
+
return lines.join("\n") + formatCorpusCoverage(res.corpus_coverage);
|
|
431
|
+
}
|
|
432
|
+
export function formatStreetList(res, q, scope) {
|
|
433
|
+
if (res.data.length === 0) {
|
|
434
|
+
return `No street name in ${scope} matches "${q}". The scope is valid and nothing we hold there contains that fragment — two different things can put you here, and this answer cannot tell them apart:\n`
|
|
435
|
+
+ ` 1. We hold no street addresses in this area at all. Coverage follows the addresses themselves and is thin outside towns, so rural parcels are often there while their streets are not — for one of those, resolve_parcel with the precinct name and the parcel number is the way in, not a street.\n`
|
|
436
|
+
+ ` 2. There is genuinely no such street here.\n`
|
|
437
|
+
+ `Matching is case- and accent-insensitive but does not inflect, so pass the name in the nominative ('karmelicka' or 'Karmelicką' both find 'Karmelicka', but 'Karmelickiej' does not).`;
|
|
438
|
+
}
|
|
439
|
+
const lines = [
|
|
440
|
+
`${res.data.length} street name${res.data.length === 1 ? "" : "s"} in ${scope} matching "${q}":\n`,
|
|
441
|
+
];
|
|
442
|
+
for (const row of res.data) {
|
|
443
|
+
const tag = row.address_source === "approx" ? " [approximate — derived, not from the record]" : "";
|
|
444
|
+
lines.push(` - ${row.street}${tag}`);
|
|
445
|
+
}
|
|
446
|
+
lines.push("\nUntagged names are on record; tagged ones we worked out for parcels the record left without a street. " +
|
|
447
|
+
"Any of these can be passed to list_parcels_in_area as street= inside the same scope.");
|
|
448
|
+
if (res.pagination.has_more) {
|
|
449
|
+
lines.push(`More names match than are shown (the answer is capped at ${res.pagination.limit}). Type more of the name — there is no second page.`);
|
|
450
|
+
}
|
|
304
451
|
return lines.join("\n");
|
|
305
452
|
}
|
|
453
|
+
export function formatParcelFeatures(res, scope) {
|
|
454
|
+
if (res.features.length === 0) {
|
|
455
|
+
return `No parcels found in ${scope}. The area is valid and holds no parcel WE CAN PLACE THERE — coverage is near-complete but not the whole cadastral register and not live, so read this as "we have nothing mapped here", not as "there is nothing here". ${CORPUS_COVERAGE_HINT}${formatCorpusCoverage(res.corpus_coverage)}`;
|
|
456
|
+
}
|
|
457
|
+
const lines = [`Found ${res.features.length} parcel${res.features.length === 1 ? "" : "s"} in ${scope}:`];
|
|
458
|
+
if (res.truncated) {
|
|
459
|
+
lines.push(`TRUNCATED — more of the parcels we hold match than the limit returns, and the ones below are an arbitrary subset, not the first, nearest or largest. Read this as a sample of the area, never as its parcel list. A smaller area returns everything WE HOLD there, which is not the same as every parcel there: coverage is near-complete but not the whole cadastral register and not live.`);
|
|
460
|
+
}
|
|
461
|
+
lines.push("");
|
|
462
|
+
for (const [i, f] of res.features.entries()) {
|
|
463
|
+
const district = f.properties.district ?? "Unknown";
|
|
464
|
+
const centre = outlineCentre(f.geometry);
|
|
465
|
+
const where = centre ? `${centre.lat.toFixed(4)}°N, ${centre.lng.toFixed(4)}°E` : "unknown";
|
|
466
|
+
lines.push(`${i + 1}. ${f.properties.parcel_id ?? PARCEL_IDENTITY_WITHHELD}`);
|
|
467
|
+
lines.push(` District: ${district} | Outline centre: ${where}`);
|
|
468
|
+
lines.push(` Outline (GeoJSON, WGS84): ${f.geometry ? JSON.stringify(f.geometry) : "not returned"}`);
|
|
469
|
+
}
|
|
470
|
+
return lines.join("\n") + formatCorpusCoverage(res.corpus_coverage);
|
|
471
|
+
}
|
|
306
472
|
export function formatParcelResolve(res) {
|
|
473
|
+
if (res.coverage === "not_computed") {
|
|
474
|
+
return "The lookup could not be completed, so this says nothing about whether the parcel exists (the credit is refunded). Best next step: pass the full cadastral id in parcelId — that path does not go through the name at all. A retry helps only if a live confirmation timed out; it will not change the answer for a name carried by very many precincts.";
|
|
475
|
+
}
|
|
307
476
|
if (res.coverage === "not_covered" || res.matches.length === 0) {
|
|
308
|
-
return "No parcel matched.
|
|
477
|
+
return "No parcel matched (the credit is refunded). Two different causes, and with near-complete coverage the first is now the common one: the name may be spelled differently than the register spells it (for 'name + number' it is matched against the gmina name and the cadastral precinct (obręb) name, exactly), OR we do not hold the parcel — coverage is near-complete but not the whole cadastral register and not live, so a discovery lookup can still miss a parcel that exists. This is not a confirmation that the parcel does not exist. Passing the FULL cadastral id in parcelId goes down a different path that can confirm and add a parcel we do not yet hold." + formatCorpusCoverage(res.corpus_coverage);
|
|
309
478
|
}
|
|
310
479
|
const lines = [`Found ${res.matches.length} parcel${res.matches.length === 1 ? "" : "s"}:\n`];
|
|
311
480
|
for (const [i, m] of res.matches.entries()) {
|
|
@@ -322,7 +491,8 @@ export function formatParcelResolve(res) {
|
|
|
322
491
|
if (res.as_of) {
|
|
323
492
|
lines.push(`\nCadastral copy as of ${res.as_of.split("T")[0]}.`);
|
|
324
493
|
}
|
|
325
|
-
|
|
494
|
+
lines.push(`\nQuery cost: 0 API tokens — resolving a parcel is free.`);
|
|
495
|
+
return lines.join("\n") + formatCorpusCoverage(res.corpus_coverage);
|
|
326
496
|
}
|
|
327
497
|
function formatSpatialFeature(f) {
|
|
328
498
|
return formatTransactionCore({
|
|
@@ -355,6 +525,37 @@ export function formatSpatialResults(res) {
|
|
|
355
525
|
}
|
|
356
526
|
return lines.join("\n");
|
|
357
527
|
}
|
|
528
|
+
export function formatBuildingAge(age) {
|
|
529
|
+
if (age == null)
|
|
530
|
+
return "";
|
|
531
|
+
const works = age.last_works_year != null ? `; later works ${age.last_works_year}` : "";
|
|
532
|
+
const clause = (body) => `construction year not established: ${body}${works}`;
|
|
533
|
+
switch (age.status) {
|
|
534
|
+
case "estimated": {
|
|
535
|
+
if (age.year_from == null || age.year_to == null) {
|
|
536
|
+
return clause("an estimate came back without the interval it has to carry, so there is no honest year to state");
|
|
537
|
+
}
|
|
538
|
+
const range = age.year_from === age.year_to ? `${age.year_from}` : `${age.year_from}-${age.year_to}`;
|
|
539
|
+
const point = age.year_point != null ? `, point estimate ~${age.year_point} (least certain)` : "";
|
|
540
|
+
const conf = age.confidence ? `, ${age.confidence} confidence in the permit match (not in the year)` : "";
|
|
541
|
+
return `built range ${range}${point} [estimate from permit records, not a registry date${conf}]${works}`;
|
|
542
|
+
}
|
|
543
|
+
case "older_than_register":
|
|
544
|
+
return clause("no construction record for this land falls inside our coverage, which starts in 2016 — the works may predate it, or the land may have been renumbered since");
|
|
545
|
+
case "ambiguous_permits":
|
|
546
|
+
case "ambiguous_buildings":
|
|
547
|
+
case "ambiguous_both":
|
|
548
|
+
return clause("construction records for this land could not be tied to this specific building");
|
|
549
|
+
case "no_parcel_key":
|
|
550
|
+
return clause("this land has no cadastral identifier to match records against");
|
|
551
|
+
case "not_applicable":
|
|
552
|
+
return clause("this building has no outline we can place on the land");
|
|
553
|
+
case "not_computed":
|
|
554
|
+
return clause("the lookup could not be completed for this request — retrying later may return one");
|
|
555
|
+
default:
|
|
556
|
+
return clause("the answer came back in a form this client version does not recognise — a newer client may be able to state it");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
358
559
|
export function formatBuildingBreakdown(res) {
|
|
359
560
|
const { data, truncated } = res;
|
|
360
561
|
if (data.length === 0) {
|
|
@@ -380,6 +581,9 @@ export function formatBuildingBreakdown(res) {
|
|
|
380
581
|
}
|
|
381
582
|
if (b.match_confidence)
|
|
382
583
|
cells.push(`match confidence: ${b.match_confidence}`);
|
|
584
|
+
const age = formatBuildingAge(b.age_estimate);
|
|
585
|
+
if (age)
|
|
586
|
+
cells.push(age);
|
|
383
587
|
lines.push(`${i + 1}. ${cells.join(" | ")}`);
|
|
384
588
|
});
|
|
385
589
|
if (truncated) {
|
|
@@ -498,6 +702,155 @@ export function formatLandslideBreakdown(res) {
|
|
|
498
702
|
lines.push("", "Note: based on official landslide-hazard maps (1:10,000 scale). An intersection means the parcel overlaps a mapped hazard area, not that the parcel itself is a landslide.");
|
|
499
703
|
return lines.join("\n");
|
|
500
704
|
}
|
|
705
|
+
const PROTECTION_RANK_LABEL = {
|
|
706
|
+
1: "national park",
|
|
707
|
+
2: "nature reserve",
|
|
708
|
+
3: "Natura 2000",
|
|
709
|
+
4: "landscape park",
|
|
710
|
+
5: "protected landscape",
|
|
711
|
+
6: "a minor protection form, or a buffer zone",
|
|
712
|
+
};
|
|
713
|
+
const BUILDING_RESTRICTION_NOTE = {
|
|
714
|
+
statutory_ban: "a build ban that follows directly from the Nature Protection Act (national parks and reserves), with statutory exceptions — not a ruling on any specific project",
|
|
715
|
+
conditional: "no blanket statutory ban; restrictions depend on the act that established the area",
|
|
716
|
+
};
|
|
717
|
+
const NATURE_EMPTY_MESSAGE = {
|
|
718
|
+
covered_no_data: "No forest within 2 km and no protected natural area overlaps this transaction's parcels (or the id was not found). An empty result is never a statement that building is allowed — this layer does not cover local zoning plans, planning-permission decisions or areas under designation.",
|
|
719
|
+
not_covered: "This layer holds no nature reference data for these parcels yet, so nothing was checked. That is NOT a finding that there is no forest nearby and no protected natural area here — it means the check could not be made. The credit for this call is refunded; try again later.",
|
|
720
|
+
unknown: "No forest or protected-area signal was returned for this transaction's parcels. This response does not distinguish 'checked, nothing found' from 'not checked' (the id may also be unknown), so do not read it as a finding that there is no forest or protected area — and never as a statement that building is allowed.",
|
|
721
|
+
};
|
|
722
|
+
export function formatNatureBreakdown(res) {
|
|
723
|
+
const { data, truncated } = res;
|
|
724
|
+
if (data.length === 0) {
|
|
725
|
+
const key = res.coverage === "covered_no_data" || res.coverage === "not_covered" ? res.coverage : "unknown";
|
|
726
|
+
return NATURE_EMPTY_MESSAGE[key];
|
|
727
|
+
}
|
|
728
|
+
const lines = [
|
|
729
|
+
`Per-parcel nature breakdown (${data.length} parcel${data.length === 1 ? "" : "s"} with a forest or protected-area signal):`,
|
|
730
|
+
"",
|
|
731
|
+
];
|
|
732
|
+
data.forEach((r, i) => {
|
|
733
|
+
const cells = [];
|
|
734
|
+
const dist = r.forest_distance_m != null ? Number(r.forest_distance_m) : null;
|
|
735
|
+
if (dist != null && Number.isFinite(dist)) {
|
|
736
|
+
if (dist === 0) {
|
|
737
|
+
const ov = r.forest_overlap_pct != null ? Number(r.forest_overlap_pct) : null;
|
|
738
|
+
cells.push(`forest: overlaps the parcel${ov != null && Number.isFinite(ov) ? ` (${Math.round(ov)}% of area)` : ""}`);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
cells.push(`forest: ${dist} m away`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (r.protection_rank != null) {
|
|
745
|
+
const label = PROTECTION_RANK_LABEL[r.protection_rank] ?? `protection rank ${r.protection_rank}`;
|
|
746
|
+
cells.push(`protection: ${label}`);
|
|
747
|
+
if (r.building_restriction) {
|
|
748
|
+
const note = BUILDING_RESTRICTION_NOTE[r.building_restriction];
|
|
749
|
+
cells.push(`build restriction: ${r.building_restriction}${note ? ` (${note})` : ""}`);
|
|
750
|
+
}
|
|
751
|
+
if (r.protected_overlap_pct != null) {
|
|
752
|
+
const p = Number(r.protected_overlap_pct);
|
|
753
|
+
if (Number.isFinite(p))
|
|
754
|
+
cells.push(`${Math.round(p)}% of the parcel under protection`);
|
|
755
|
+
}
|
|
756
|
+
if (Array.isArray(r.protected_areas) && r.protected_areas.length > 0) {
|
|
757
|
+
const labels = r.protected_areas.map((a) => a.name ?? a.form).filter((x) => !!x);
|
|
758
|
+
if (labels.length > 0)
|
|
759
|
+
cells.push(`areas: ${labels.join("; ")}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
lines.push(`${i + 1}. ${cells.join(" | ")}`);
|
|
763
|
+
});
|
|
764
|
+
if (truncated) {
|
|
765
|
+
lines.push("", "Showing the first 500 parcels (the transaction is linked to more).");
|
|
766
|
+
}
|
|
767
|
+
lines.push("", "Note: forest_distance_m is the nearest forest within 2 km (0 = the parcel overlaps forest). building_restriction names the SOURCE of the restriction (statute vs the act that established the area), not the outcome of any permitting case; an empty result is never a statement that building is allowed — this layer does not cover local zoning plans, planning-permission decisions or areas under designation. A buffer zone around a park or reserve IS reported, as form 'buffer_zone' at rank 6, and never as a statutory ban. Forest coverage is mainly publicly-managed land, so private forests may be incomplete.");
|
|
768
|
+
return lines.join("\n");
|
|
769
|
+
}
|
|
770
|
+
const MINERAL_CLASS_NOTE = {
|
|
771
|
+
subsidence: "extraction with surface deformation — the actual mining-damage risk",
|
|
772
|
+
surface: "open-pit working — mostly local impact (neighbourhood, noise)",
|
|
773
|
+
fluid: "borehole extraction — usually no surface deformation, but a concession and zones still apply",
|
|
774
|
+
other: "mineral not classified",
|
|
775
|
+
};
|
|
776
|
+
const SUBSURFACE_NOTE = "Note: a mining terrain is a legally defined zone of anticipated mining influence; its mapped location is approximate — an intersection is an advisory signal to verify with the competent mining-supervision authority (named per terrain as the oversight authority), not a legal determination. Some terrains with status 'active' carry a valid_until already in the past, because the register entry can lag behind the expiry of a concession. A groundwater reservoir's extent alone imposes NO restriction; a restriction would come only from an established protection zone, which is not published here. Absence of a match is never asserted as 'safe': the mining register covers concession areas, so historic or shallow workings may not appear, and an absent reservoir does not mean there is no groundwater beneath the parcel.";
|
|
777
|
+
function subsurfacePct(v) {
|
|
778
|
+
if (v == null)
|
|
779
|
+
return null;
|
|
780
|
+
const n = Number(v);
|
|
781
|
+
return Number.isFinite(n) ? Math.round(n) : null;
|
|
782
|
+
}
|
|
783
|
+
export function formatSubsurfaceBreakdown(res) {
|
|
784
|
+
const { data, truncated } = res;
|
|
785
|
+
if (data.length === 0) {
|
|
786
|
+
return "No mapped mining terrain or major groundwater reservoir overlaps this transaction's parcels (or the id was not found). Absence of mapped data is never asserted as 'safe' — the mining register covers concession areas, so historic or shallow workings may not appear.";
|
|
787
|
+
}
|
|
788
|
+
const lines = [
|
|
789
|
+
`Per-parcel subsurface breakdown (${data.length} parcel${data.length === 1 ? "" : "s"} overlapping a mining terrain or a major groundwater reservoir):`,
|
|
790
|
+
"",
|
|
791
|
+
];
|
|
792
|
+
let n = 0;
|
|
793
|
+
for (const r of data) {
|
|
794
|
+
const cells = [];
|
|
795
|
+
if (r.mining_status) {
|
|
796
|
+
const cls = r.mineral_class ?? null;
|
|
797
|
+
const clsNote = cls ? MINERAL_CLASS_NOTE[cls] : undefined;
|
|
798
|
+
const pct = subsurfacePct(r.mining_overlap_pct);
|
|
799
|
+
cells.push(`mining terrain: ${r.mining_status}${cls ? `, ${cls}${clsNote ? ` (${clsNote})` : ""}` : ""}${pct != null ? `, ${pct}% of the parcel in the terrain` : ""}`);
|
|
800
|
+
if (Array.isArray(r.mining_terrains) && r.mining_terrains.length > 0) {
|
|
801
|
+
const labels = r.mining_terrains
|
|
802
|
+
.map((t) => {
|
|
803
|
+
const parts = [];
|
|
804
|
+
if (t.name)
|
|
805
|
+
parts.push(t.name);
|
|
806
|
+
if (t.oversight_authority)
|
|
807
|
+
parts.push(`oversight: ${t.oversight_authority}`);
|
|
808
|
+
if (t.valid_until)
|
|
809
|
+
parts.push(`valid until ${t.valid_until}`);
|
|
810
|
+
if (t.revoked_on)
|
|
811
|
+
parts.push(`revoked ${t.revoked_on}`);
|
|
812
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
813
|
+
})
|
|
814
|
+
.filter((s) => !!s);
|
|
815
|
+
if (labels.length > 0)
|
|
816
|
+
cells.push(`terrains: ${labels.join("; ")}${r.mining_terrains_capped ? " (list reached the 5-entry cap)" : ""}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (r.groundwater_status) {
|
|
820
|
+
const pct = subsurfacePct(r.groundwater_overlap_pct);
|
|
821
|
+
cells.push(`groundwater reservoir: ${r.groundwater_status} (reservoir extent only — not a restriction)${pct != null ? `, ${pct}% of the parcel in the reservoir` : ""}`);
|
|
822
|
+
if (Array.isArray(r.groundwater_bodies) && r.groundwater_bodies.length > 0) {
|
|
823
|
+
const labels = r.groundwater_bodies
|
|
824
|
+
.map((b) => {
|
|
825
|
+
const parts = [];
|
|
826
|
+
if (b.number != null)
|
|
827
|
+
parts.push(`no. ${b.number}`);
|
|
828
|
+
if (b.name)
|
|
829
|
+
parts.push(b.name);
|
|
830
|
+
if (b.documented_year != null)
|
|
831
|
+
parts.push(`documented ${b.documented_year}`);
|
|
832
|
+
if (b.depth_from_m != null)
|
|
833
|
+
parts.push(`from ${b.depth_from_m} m`);
|
|
834
|
+
if (b.medium_type)
|
|
835
|
+
parts.push(b.medium_type);
|
|
836
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
837
|
+
})
|
|
838
|
+
.filter((s) => !!s);
|
|
839
|
+
if (labels.length > 0)
|
|
840
|
+
cells.push(`reservoirs: ${labels.join("; ")}${r.groundwater_bodies_capped ? " (list reached the 5-entry cap)" : ""}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (cells.length === 0)
|
|
844
|
+
continue;
|
|
845
|
+
n += 1;
|
|
846
|
+
lines.push(`${n}. ${cells.join(" | ")}`);
|
|
847
|
+
}
|
|
848
|
+
if (truncated) {
|
|
849
|
+
lines.push("", "Showing the first 500 parcels (the transaction is linked to more).");
|
|
850
|
+
}
|
|
851
|
+
lines.push("", SUBSURFACE_NOTE);
|
|
852
|
+
return lines.join("\n");
|
|
853
|
+
}
|
|
501
854
|
const SURROUNDINGS_CATEGORIES = [
|
|
502
855
|
{ key: "cemetery_distance_m", label: "cemetery", radiusLabel: "1 km" },
|
|
503
856
|
{ key: "landfill_distance_m", label: "landfill (waste disposal)", radiusLabel: "3 km" },
|
|
@@ -505,6 +858,8 @@ const SURROUNDINGS_CATEGORIES = [
|
|
|
505
858
|
{ key: "industrial_area_distance_m", label: "industrial/storage area", radiusLabel: "1 km" },
|
|
506
859
|
{ key: "industrial_plant_distance_m", label: "large industrial plant", radiusLabel: "3 km" },
|
|
507
860
|
{ key: "livestock_farm_distance_m", label: "intensive livestock farm", radiusLabel: "3 km" },
|
|
861
|
+
{ key: "power_line_hv_distance_m", label: "high-voltage overhead power line", radiusLabel: "1 km" },
|
|
862
|
+
{ key: "power_line_ehv_distance_m", label: "extra-high-voltage overhead power line", radiusLabel: "1 km" },
|
|
508
863
|
];
|
|
509
864
|
export function formatSurroundings(res) {
|
|
510
865
|
const { data, truncated } = res;
|
|
@@ -520,7 +875,7 @@ export function formatSurroundings(res) {
|
|
|
520
875
|
lines.push(`${i + 1}. not assessed yet — this plot has not been evaluated (no statement either way)`);
|
|
521
876
|
return;
|
|
522
877
|
}
|
|
523
|
-
const cells = SURROUNDINGS_CATEGORIES.map(({ key, label, radiusLabel }) => {
|
|
878
|
+
const cells = SURROUNDINGS_CATEGORIES.filter(({ key }) => key in r).map(({ key, label, radiusLabel }) => {
|
|
524
879
|
const raw = r[key];
|
|
525
880
|
const dist = raw == null ? null : Number(raw);
|
|
526
881
|
if (dist == null || !Number.isFinite(dist)) {
|
|
@@ -537,6 +892,64 @@ export function formatSurroundings(res) {
|
|
|
537
892
|
}
|
|
538
893
|
return lines.join("\n");
|
|
539
894
|
}
|
|
895
|
+
const ROADS_NOTE = "Note: access_indicator is geometric evidence measured from carriageway centrelines in reference road-network data. It does NOT determine legal access and says nothing about easements or rights of way, which are recorded in the land register and are not published here. Distances are approximate and measured from the plot boundary; a public road and a road of any kind are searched within 500 m, a motorway/expressway/dual-carriageway (a traffic-nuisance proxy, not access) within 3 km. A null distance means nothing of that kind within that radius — never a guarantee of absence.";
|
|
896
|
+
const ROAD_INDICATOR_GLOSS = {
|
|
897
|
+
likely: "access likely",
|
|
898
|
+
uncertain: "access uncertain",
|
|
899
|
+
unlikely: "access unlikely",
|
|
900
|
+
};
|
|
901
|
+
function roadCells(r) {
|
|
902
|
+
const cells = [];
|
|
903
|
+
const indicator = typeof r.access_indicator === "string" ? r.access_indicator : null;
|
|
904
|
+
cells.push(indicator ? ROAD_INDICATOR_GLOSS[indicator] ?? `access ${indicator}` : "access not classified");
|
|
905
|
+
if (typeof r.access_rule_version === "number")
|
|
906
|
+
cells.push(`rule v${r.access_rule_version}`);
|
|
907
|
+
const pub = toNum(r.public_road_distance_m);
|
|
908
|
+
const edge = toNum(r.public_road_edge_distance_m);
|
|
909
|
+
if (pub == null) {
|
|
910
|
+
cells.push("public road: none within 500 m");
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
const kind = [r.public_road_category, r.public_road_class].filter((x) => typeof x === "string");
|
|
914
|
+
const edgePart = edge != null ? `, ~${Math.round(edge)} m to the carriageway edge` : "";
|
|
915
|
+
cells.push(`public road: ~${Math.round(pub)} m${kind.length > 0 ? ` (${kind.join(", ")})` : ""}${edgePart}`);
|
|
916
|
+
if (r.public_road_at_grade === false)
|
|
917
|
+
cells.push("that road crosses on a viaduct or in a tunnel");
|
|
918
|
+
}
|
|
919
|
+
const any = toNum(r.any_road_distance_m);
|
|
920
|
+
cells.push(any == null ? "any road: none within 500 m" : `any road: ~${Math.round(any)} m`);
|
|
921
|
+
const major = toNum(r.major_road_distance_m);
|
|
922
|
+
cells.push(major == null ? "major road: none within 3 km" : `major road: ~${Math.round(major)} m`);
|
|
923
|
+
return cells;
|
|
924
|
+
}
|
|
925
|
+
export function formatRoads(res) {
|
|
926
|
+
const { data, truncated } = res;
|
|
927
|
+
if (data.length === 0) {
|
|
928
|
+
return "No road-access data is available for this transaction (no linked plots, or the id was not found).";
|
|
929
|
+
}
|
|
930
|
+
const lines = [
|
|
931
|
+
`Per-parcel road access (${data.length} plot${data.length === 1 ? "" : "s"}; distances from the plot boundary, "~" = approximate):`,
|
|
932
|
+
"",
|
|
933
|
+
];
|
|
934
|
+
data.forEach((r, i) => {
|
|
935
|
+
if (!r.assessed) {
|
|
936
|
+
lines.push(`${i + 1}. not assessed yet — this plot has not been evaluated (no statement either way)`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
lines.push(`${i + 1}. ${roadCells(r).join(" | ")}`);
|
|
940
|
+
});
|
|
941
|
+
if (truncated) {
|
|
942
|
+
lines.push("", "Showing the first 500 plots (the transaction is linked to more).");
|
|
943
|
+
}
|
|
944
|
+
const dates = data
|
|
945
|
+
.map((r) => r.source_as_of)
|
|
946
|
+
.filter((d) => typeof d === "string" && d.length > 0)
|
|
947
|
+
.sort();
|
|
948
|
+
if (dates.length > 0)
|
|
949
|
+
lines.push("", `Reference road-network snapshot as of ${dates[0]} (oldest of the plots shown).`);
|
|
950
|
+
lines.push("", ROADS_NOTE);
|
|
951
|
+
return lines.join("\n");
|
|
952
|
+
}
|
|
540
953
|
const TRANSIT_COVERAGE_NOTE = "Note: distances are from open public-transport schedules (GTFS format); coverage is cities and national rail, not every rural area. A mode missing above means no stop of that mode was found within its distance cap — never read as 'no public transport access'.";
|
|
541
954
|
export function formatTransitBreakdown(res) {
|
|
542
955
|
const { data, truncated } = res;
|
|
@@ -565,7 +978,7 @@ export function formatTransitBreakdown(res) {
|
|
|
565
978
|
lines.push("", TRANSIT_COVERAGE_NOTE);
|
|
566
979
|
return lines.join("\n");
|
|
567
980
|
}
|
|
568
|
-
const PERMITS_EMPTY_NOTE = "No
|
|
981
|
+
const PERMITS_EMPTY_NOTE = "No building permit or works notification is on record for this transaction's parcels (or the id was not found). Permits are held once a decision has been issued, and notifications only where they were accepted without objection; cases registered since 2016 are matched by the parcel's current identifier — an empty list is never a statement that nothing was ever planned.";
|
|
569
982
|
export function formatPermitsBreakdown(res) {
|
|
570
983
|
const { data, truncated } = res;
|
|
571
984
|
if (data.length === 0) {
|
|
@@ -765,6 +1178,74 @@ export function formatLocationHierarchy(items, parent) {
|
|
|
765
1178
|
}
|
|
766
1179
|
return lines.join("\n");
|
|
767
1180
|
}
|
|
1181
|
+
const SEARCH_LEVEL_ORDER = {
|
|
1182
|
+
voivodeship: 0,
|
|
1183
|
+
county: 1,
|
|
1184
|
+
municipality: 2,
|
|
1185
|
+
precinct: 3,
|
|
1186
|
+
};
|
|
1187
|
+
const SEARCH_LEVEL_LABEL = {
|
|
1188
|
+
voivodeship: "Voivodeships",
|
|
1189
|
+
county: "Counties",
|
|
1190
|
+
municipality: "Municipalities",
|
|
1191
|
+
precinct: "Precincts",
|
|
1192
|
+
};
|
|
1193
|
+
function searchCallsForItem(it) {
|
|
1194
|
+
const code = it.code;
|
|
1195
|
+
const calls = [
|
|
1196
|
+
`search_transactions(teryt="${code}")`,
|
|
1197
|
+
`list_parcels_in_area(teryt="${code}")`,
|
|
1198
|
+
];
|
|
1199
|
+
if (it.level === "county" || it.level === "municipality") {
|
|
1200
|
+
calls.push(`list_locations(parent="${code}")`);
|
|
1201
|
+
}
|
|
1202
|
+
if (it.level !== "precinct") {
|
|
1203
|
+
calls.push(`get_demographics(teryt="${code}")`);
|
|
1204
|
+
}
|
|
1205
|
+
if (it.level === "county" || it.level === "municipality") {
|
|
1206
|
+
calls.push(`get_infrastructure_signals(teryt="${code}")`);
|
|
1207
|
+
}
|
|
1208
|
+
if (it.rcn_district) {
|
|
1209
|
+
calls.push(`search_transactions(location="${it.name}")`);
|
|
1210
|
+
calls.push(`compare_locations(districts="${it.name},…")`);
|
|
1211
|
+
calls.push(`get_price_statistics(location="${it.name}")`);
|
|
1212
|
+
}
|
|
1213
|
+
return calls;
|
|
1214
|
+
}
|
|
1215
|
+
export function formatLocationSearch(items, rcnOnly, query) {
|
|
1216
|
+
const lines = [`Location search for "${query}":`, ""];
|
|
1217
|
+
const byLevel = new Map();
|
|
1218
|
+
for (const it of items) {
|
|
1219
|
+
const bucket = byLevel.get(it.level) ?? [];
|
|
1220
|
+
bucket.push(it);
|
|
1221
|
+
byLevel.set(it.level, bucket);
|
|
1222
|
+
}
|
|
1223
|
+
const levels = [...byLevel.keys()].sort((a, b) => (SEARCH_LEVEL_ORDER[a] ?? 99) - (SEARCH_LEVEL_ORDER[b] ?? 99));
|
|
1224
|
+
for (const level of levels) {
|
|
1225
|
+
lines.push(`${SEARCH_LEVEL_LABEL[level] ?? level}:`);
|
|
1226
|
+
for (const it of byLevel.get(level)) {
|
|
1227
|
+
const typeSuffix = it.typeName ? ` (${it.typeName})` : "";
|
|
1228
|
+
const parentSuffix = it.parent_name ? `, ${it.parent_name}` : "";
|
|
1229
|
+
lines.push(` ${it.code} - ${it.name}${typeSuffix}${parentSuffix}`);
|
|
1230
|
+
for (const call of searchCallsForItem(it)) {
|
|
1231
|
+
lines.push(` ${call}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
lines.push("");
|
|
1235
|
+
}
|
|
1236
|
+
if (rcnOnly.length > 0) {
|
|
1237
|
+
lines.push("RCN district names without a TERYT code:");
|
|
1238
|
+
for (const name of rcnOnly) {
|
|
1239
|
+
lines.push(` - ${name}`);
|
|
1240
|
+
lines.push(` search_transactions(location="${name}")`);
|
|
1241
|
+
}
|
|
1242
|
+
lines.push("");
|
|
1243
|
+
}
|
|
1244
|
+
if (items.length === 50) {
|
|
1245
|
+
lines.push("Showing the first 50 matches — type more letters to narrow the search.");
|
|
1246
|
+
}
|
|
1247
|
+
return lines.join("\n").trimEnd();
|
|
1248
|
+
}
|
|
768
1249
|
export function formatCompareResults(res) {
|
|
769
1250
|
const districts = Object.keys(res);
|
|
770
1251
|
if (districts.length === 0) {
|
|
@@ -1067,6 +1548,49 @@ export function formatPriceSpreadLocations(r) {
|
|
|
1067
1548
|
}
|
|
1068
1549
|
return lines.join("\n");
|
|
1069
1550
|
}
|
|
1551
|
+
const FLOOD_RISK_LOCATIONS_PATH = "flood-risk/locations";
|
|
1552
|
+
export function formatFloodRisk(r) {
|
|
1553
|
+
const q = r.quality;
|
|
1554
|
+
const sev = r.result.by_severity;
|
|
1555
|
+
const lines = [`Flood-hazard exposure — ${r.location.name}`, ""];
|
|
1556
|
+
lines.push(r.result.flood_share_pct != null
|
|
1557
|
+
? `Share in a mapped flood-hazard zone: ${r.result.flood_share_pct}% of assessed transactions`
|
|
1558
|
+
: `Share: N/A (coverage: ${q.coverage})`);
|
|
1559
|
+
if (sev.low != null || sev.medium != null || sev.high != null) {
|
|
1560
|
+
lines.push("");
|
|
1561
|
+
lines.push("Transactions by severity (return period):");
|
|
1562
|
+
lines.push(` High (~1-in-10-year): ${formatNumber(sev.high ?? 0)}`);
|
|
1563
|
+
lines.push(` Medium (~1-in-100-year): ${formatNumber(sev.medium ?? 0)}`);
|
|
1564
|
+
lines.push(` Low (~1-in-500-year): ${formatNumber(sev.low ?? 0)}`);
|
|
1565
|
+
}
|
|
1566
|
+
lines.push("");
|
|
1567
|
+
lines.push(`Assessed transactions: ${r.inputs.assessed_sample_n != null ? formatNumber(r.inputs.assessed_sample_n) : "N/A"} (all-time)`);
|
|
1568
|
+
lines.push(`Coverage: ${q.coverage} | Confidence: ${q.confidence}${q.stale ? " | transaction data lags publication" : ""}`);
|
|
1569
|
+
if (q.as_of)
|
|
1570
|
+
lines.push(`Transaction data as of: ${q.as_of}`);
|
|
1571
|
+
const visibleNotes = q.notes.filter((n) => !n.includes(FLOOD_RISK_LOCATIONS_PATH));
|
|
1572
|
+
if (visibleNotes.length > 0) {
|
|
1573
|
+
lines.push("", "Notes:");
|
|
1574
|
+
for (const n of visibleNotes)
|
|
1575
|
+
lines.push(` - ${n}`);
|
|
1576
|
+
}
|
|
1577
|
+
return lines.join("\n");
|
|
1578
|
+
}
|
|
1579
|
+
export function formatFloodRiskLocations(r) {
|
|
1580
|
+
const { data, meta } = r;
|
|
1581
|
+
if (data.length === 0) {
|
|
1582
|
+
return "No flood-risk-covered locations match.";
|
|
1583
|
+
}
|
|
1584
|
+
const dateSuffix = meta.snapshot_date ? `, data from ${meta.snapshot_date}` : "";
|
|
1585
|
+
const lines = [
|
|
1586
|
+
`Flood-risk coverage — ${meta.total} location${meta.total === 1 ? "" : "s"}${dateSuffix}`,
|
|
1587
|
+
"",
|
|
1588
|
+
];
|
|
1589
|
+
for (const loc of data) {
|
|
1590
|
+
lines.push(`- ${loc.location} (teryt ${loc.county_code}, ${loc.voivodeship}, ${loc.type}) — n=${formatNumber(loc.assessed_sample_n)}, ${loc.confidence} confidence`);
|
|
1591
|
+
}
|
|
1592
|
+
return lines.join("\n");
|
|
1593
|
+
}
|
|
1070
1594
|
function valuationCompLine(c) {
|
|
1071
1595
|
const parts = [`${formatNumber(c.distance_m)} m`, c.transaction_date, formatArea(c.area_m2), `${formatPLN(c.price_per_m2)}/m²`];
|
|
1072
1596
|
if (c.market_type)
|
|
@@ -1131,6 +1655,15 @@ function fourStateGloss(coverage) {
|
|
|
1131
1655
|
default: return coverage;
|
|
1132
1656
|
}
|
|
1133
1657
|
}
|
|
1658
|
+
function stringList(v) {
|
|
1659
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
1660
|
+
}
|
|
1661
|
+
function landClassHeadline(useNames, soilClasses) {
|
|
1662
|
+
return [
|
|
1663
|
+
useNames.length ? useNames.join(", ") : null,
|
|
1664
|
+
soilClasses.length ? `soil class ${soilClasses.join(", ")}` : null,
|
|
1665
|
+
].filter(Boolean).join("; ");
|
|
1666
|
+
}
|
|
1134
1667
|
function toNum(v) {
|
|
1135
1668
|
if (v == null)
|
|
1136
1669
|
return null;
|
|
@@ -1165,6 +1698,19 @@ function reportSectionDetail(name, s) {
|
|
|
1165
1698
|
const risk = typeof s.landslide_risk === "string" ? s.landslide_risk : null;
|
|
1166
1699
|
return risk ? LANDSLIDE_RISK_NOTE[risk] ?? risk : "";
|
|
1167
1700
|
}
|
|
1701
|
+
case "subsurface": {
|
|
1702
|
+
if (!covered)
|
|
1703
|
+
return "";
|
|
1704
|
+
const parts = [];
|
|
1705
|
+
const mining = typeof s.mining_status === "string" ? s.mining_status : null;
|
|
1706
|
+
const mineral = typeof s.mineral_class === "string" ? s.mineral_class : null;
|
|
1707
|
+
if (mining)
|
|
1708
|
+
parts.push(`mining terrain ${mining}${mineral ? ` (${mineral})` : ""}`);
|
|
1709
|
+
const gw = typeof s.groundwater_status === "string" ? s.groundwater_status : null;
|
|
1710
|
+
if (gw)
|
|
1711
|
+
parts.push(`groundwater reservoir ${gw}`);
|
|
1712
|
+
return parts.join("; ");
|
|
1713
|
+
}
|
|
1168
1714
|
case "surroundings": {
|
|
1169
1715
|
if (!covered)
|
|
1170
1716
|
return "";
|
|
@@ -1180,6 +1726,21 @@ function reportSectionDetail(name, s) {
|
|
|
1180
1726
|
return "no mapped nuisance object within range";
|
|
1181
1727
|
return dists.slice(0, 3).map(([k, m]) => `${k} ${Math.round(m)} m`).join(", ");
|
|
1182
1728
|
}
|
|
1729
|
+
case "roads": {
|
|
1730
|
+
if (!covered)
|
|
1731
|
+
return "";
|
|
1732
|
+
const indicator = typeof s.access_indicator === "string" ? s.access_indicator : null;
|
|
1733
|
+
if (!indicator)
|
|
1734
|
+
return "";
|
|
1735
|
+
const edge = toNum(s.public_road_edge_distance_m);
|
|
1736
|
+
const pub = toNum(s.public_road_distance_m);
|
|
1737
|
+
const gloss = ROAD_INDICATOR_GLOSS[indicator] ?? indicator;
|
|
1738
|
+
if (edge != null)
|
|
1739
|
+
return `${gloss} (~${Math.round(edge)} m to the nearest public road's carriageway edge)`;
|
|
1740
|
+
if (pub != null)
|
|
1741
|
+
return `${gloss} (nearest public road ~${Math.round(pub)} m)`;
|
|
1742
|
+
return `${gloss} (no public road within 500 m)`;
|
|
1743
|
+
}
|
|
1183
1744
|
case "transit": {
|
|
1184
1745
|
if (!covered)
|
|
1185
1746
|
return "";
|
|
@@ -1204,7 +1765,16 @@ function reportSectionDetail(name, s) {
|
|
|
1204
1765
|
if (!covered)
|
|
1205
1766
|
return "";
|
|
1206
1767
|
const rows = Array.isArray(s.data) ? s.data : [];
|
|
1207
|
-
|
|
1768
|
+
const ages = rows
|
|
1769
|
+
.map((r) => r.age_estimate?.status)
|
|
1770
|
+
.filter((x) => x != null);
|
|
1771
|
+
const dated = ages.filter((x) => x === "estimated").length;
|
|
1772
|
+
const agePart = ages.length === 0
|
|
1773
|
+
? ""
|
|
1774
|
+
: dated === 0
|
|
1775
|
+
? "; construction year not established for any of them"
|
|
1776
|
+
: `; construction year estimated for ${dated} of ${ages.length} (estimate from permit records, not a registry date)`;
|
|
1777
|
+
return `${rows.length} building(s) on the parcel${agePart}`;
|
|
1208
1778
|
}
|
|
1209
1779
|
case "permits": {
|
|
1210
1780
|
if (!covered)
|
|
@@ -1219,6 +1789,27 @@ function reportSectionDetail(name, s) {
|
|
|
1219
1789
|
const pct = toNum(s.pct_of_parcel);
|
|
1220
1790
|
return area != null ? `${formatArea(area)} eligible${pct != null ? ` (${pct}% of parcel)` : ""}` : "";
|
|
1221
1791
|
}
|
|
1792
|
+
case "land_class": {
|
|
1793
|
+
if (!covered)
|
|
1794
|
+
return "";
|
|
1795
|
+
const note = typeof s.legal_note === "string" ? s.legal_note : null;
|
|
1796
|
+
const head = landClassHeadline(stringList(s.use_names), stringList(s.soil_classes));
|
|
1797
|
+
return [head || null, note].filter(Boolean).join(" — ");
|
|
1798
|
+
}
|
|
1799
|
+
case "nature": {
|
|
1800
|
+
if (!covered)
|
|
1801
|
+
return "";
|
|
1802
|
+
const rank = toNum(s.protection_rank);
|
|
1803
|
+
const dist = toNum(s.forest_distance_m);
|
|
1804
|
+
const parts = [];
|
|
1805
|
+
if (rank != null) {
|
|
1806
|
+
const label = PROTECTION_RANK_LABEL[rank] ?? `protection rank ${rank}`;
|
|
1807
|
+
parts.push(s.building_restriction === "statutory_ban" ? `${label} (statutory build ban)` : label);
|
|
1808
|
+
}
|
|
1809
|
+
if (dist != null)
|
|
1810
|
+
parts.push(dist === 0 ? "overlaps forest" : `forest ${Math.round(dist)} m`);
|
|
1811
|
+
return parts.join(", ");
|
|
1812
|
+
}
|
|
1222
1813
|
default:
|
|
1223
1814
|
return "";
|
|
1224
1815
|
}
|
|
@@ -1253,23 +1844,27 @@ function reportBillingFooter(billing) {
|
|
|
1253
1844
|
const reason = why[billing.rule] ?? billing.rule;
|
|
1254
1845
|
return `Billing: ${billing.charged} charged, ${billing.refunded} refunded — ${reason}`;
|
|
1255
1846
|
}
|
|
1256
|
-
const REPORT_LAYER_ORDER = [
|
|
1847
|
+
export const REPORT_LAYER_ORDER = [
|
|
1257
1848
|
["flood", "Flood risk"],
|
|
1258
1849
|
["heritage", "Heritage listing"],
|
|
1259
1850
|
["landslide", "Landslide risk"],
|
|
1851
|
+
["subsurface", "Subsurface constraints"],
|
|
1260
1852
|
["surroundings", "Nuisance surroundings"],
|
|
1261
1853
|
["transit", "Public transport"],
|
|
1262
1854
|
["planning", "Planning (general plan)"],
|
|
1263
1855
|
["buildings", "Buildings"],
|
|
1264
1856
|
["permits", "Building activity"],
|
|
1265
1857
|
["farmland", "Agricultural land"],
|
|
1858
|
+
["land_class", "Land use & soil class"],
|
|
1859
|
+
["nature", "Nature (forest & protected areas)"],
|
|
1860
|
+
["roads", "Road access"],
|
|
1266
1861
|
];
|
|
1267
1862
|
export function formatParcelReport(res) {
|
|
1268
1863
|
const p = res.parcel;
|
|
1269
1864
|
const id = p.parcel_id ?? p.parcel_key ?? "(parcel id requires a paid plan)";
|
|
1270
1865
|
if (res.coverage !== "covered") {
|
|
1271
1866
|
const head = res.coverage === "not_covered"
|
|
1272
|
-
? `Parcel ${id} could not be resolved — it is not
|
|
1867
|
+
? `Parcel ${id} could not be resolved — we do not hold it. Coverage is near-complete but not the whole cadastral register and not live, so this is not a finding that the parcel does not exist.`
|
|
1273
1868
|
: res.billing.rule === "disabled"
|
|
1274
1869
|
? `The composite report is temporarily unavailable for parcel ${id}.`
|
|
1275
1870
|
: `Parcel ${id} could not be resolved right now (a live lookup did not finish — retry).`;
|
|
@@ -1292,6 +1887,8 @@ export function formatParcelReport(res) {
|
|
|
1292
1887
|
const sections = res.sections;
|
|
1293
1888
|
for (const [key, label] of REPORT_LAYER_ORDER) {
|
|
1294
1889
|
const s = sections[key];
|
|
1890
|
+
if (s == null)
|
|
1891
|
+
continue;
|
|
1295
1892
|
const detail = reportSectionDetail(key, s);
|
|
1296
1893
|
lines.push(`- ${label}: ${fourStateGloss(s.coverage)}${detail ? ` — ${detail}` : ""}`);
|
|
1297
1894
|
}
|
|
@@ -1344,3 +1941,44 @@ export function formatParcelReport(res) {
|
|
|
1344
1941
|
lines.push("", "---", reportBillingFooter(res.billing));
|
|
1345
1942
|
return lines.join("\n");
|
|
1346
1943
|
}
|
|
1944
|
+
export function formatParcelLandClass(res) {
|
|
1945
|
+
const id = res.parcel.parcel_id ?? res.parcel.parcel_key ?? res.parcel.id ?? "(identifier withheld)";
|
|
1946
|
+
const asOfDay = typeof res.as_of === "string" ? res.as_of.split("T")[0] : null;
|
|
1947
|
+
if (res.coverage === "not_computed") {
|
|
1948
|
+
return `The classification lookup for parcel ${id} could not be completed, so this says nothing about what is recorded for it (the tokens are refunded). Retry shortly.`;
|
|
1949
|
+
}
|
|
1950
|
+
if (res.coverage === "not_covered") {
|
|
1951
|
+
const why = res.note ? ` ${res.note}` : "";
|
|
1952
|
+
return `No land-use or soil-quality classification is available for parcel ${id} (the tokens are refunded). Either the county does not publish one, or we hold no classification for this parcel.${why}`;
|
|
1953
|
+
}
|
|
1954
|
+
if (res.coverage === "covered_no_data") {
|
|
1955
|
+
const asOf = asOfDay ? ` (county data as of ${asOfDay})` : "";
|
|
1956
|
+
return `The county publishes the classification, but parcel ${id} has no entry in it${asOf}. That is a checked negative, not a gap in coverage: it says nothing about what the land is, only that the county's dataset holds no entry for this parcel — and it is billed as an answer.`;
|
|
1957
|
+
}
|
|
1958
|
+
if (res.coverage !== "covered") {
|
|
1959
|
+
return `The classification for parcel ${id} came back in a state this client does not recognise (${String(res.coverage)}); it says nothing about what is recorded for the parcel.`;
|
|
1960
|
+
}
|
|
1961
|
+
const useNames = stringList(res.use_names);
|
|
1962
|
+
const soilClasses = stringList(res.soil_classes);
|
|
1963
|
+
const useCodes = stringList(res.use_codes);
|
|
1964
|
+
const lines = [`Land-use and soil-quality classification: ${id}`];
|
|
1965
|
+
const headline = landClassHeadline(useNames, soilClasses);
|
|
1966
|
+
if (headline)
|
|
1967
|
+
lines.push(headline);
|
|
1968
|
+
if (useCodes.length > 0)
|
|
1969
|
+
lines.push(`Use codes: ${useCodes.join(", ")}`);
|
|
1970
|
+
lines.push(`Protected soil grade (I-III) present: ${res.protected_class_present ? "yes" : "no"}`);
|
|
1971
|
+
lines.push(`Inside a city's administrative boundary: ${res.in_city == null ? "could not be determined from the parcel id" : res.in_city ? "yes" : "no"}`);
|
|
1972
|
+
lines.push("", "Categories and grades are listed as sets: the source records no area for any of them, so this cannot say which prevails on the parcel.");
|
|
1973
|
+
if (res.legal_note) {
|
|
1974
|
+
lines.push("", `Re-designation: ${res.legal_note}`);
|
|
1975
|
+
}
|
|
1976
|
+
if (res.legal_state_as_of) {
|
|
1977
|
+
lines.push(`Legal state verified as of ${res.legal_state_as_of}.`);
|
|
1978
|
+
}
|
|
1979
|
+
if (asOfDay)
|
|
1980
|
+
lines.push(`Classification data as of ${asOfDay}.`);
|
|
1981
|
+
if (res.note)
|
|
1982
|
+
lines.push("", res.note);
|
|
1983
|
+
return lines.join("\n");
|
|
1984
|
+
}
|