Metadata-Version: 2.4
Name: plateau-id-resolve
Version: 0.1.0
Summary: Persistent, cross-version building IDs for PLATEAU. gml:id changes every release; we recognise the same physical building across versions and mint a surrogate ID that never changes — honestly, with a match confidence.
Project-URL: Homepage, https://yodolabs.jp
Project-URL: Repository, https://github.com/pixelx-jp/plateau_id_resolve
Project-URL: Issues, https://github.com/pixelx-jp/plateau_id_resolve/issues
Author-email: "Yodo Labs (PixelX Inc. / ピクセルエックス株式会社)" <pan@yodolabs.jp>
License: MIT
License-File: LICENSE
Keywords: building-id,citygml,conflation,geospatial,gis,identity-resolution,japan,physical-ai,plateau
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: shapely>=2.0
Provides-Extra: dev
Requires-Dist: fastapi>=0.110; extra == 'dev'
Requires-Dist: httpx; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pyarrow>=12.0; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Provides-Extra: geoparquet
Requires-Dist: pyarrow>=12.0; extra == 'geoparquet'
Provides-Extra: http
Requires-Dist: fastapi>=0.110; extra == 'http'
Requires-Dist: uvicorn>=0.27; extra == 'http'
Description-Content-Type: text/markdown

<div align="center">

<a href="https://yodolabs.jp">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="assets/yodo-labs-logo-dark.svg">
    <img alt="Yodo Labs" src="assets/yodo-labs-logo.svg" height="64">
  </picture>
</a>

# plateau-id-resolve

**Persistent, cross-version building IDs for [Project PLATEAU](https://www.mlit.go.jp/plateau/).**

Recognises the same physical building across PLATEAU releases and mints a `surrogate_id` that never changes — so records pinned to it survive the `gml:id` churn.

🇯🇵 [日本語版 README](README.ja.md)

*An open-source project by [Yodo Labs](https://yodolabs.jp) · Contact [pan@yodolabs.jp](mailto:pan@yodolabs.jp)*

[![PyPI](https://img.shields.io/pypi/v/plateau-id-resolve.svg)](https://pypi.org/project/plateau-id-resolve/)
[![CI](https://github.com/pixelx-jp/plateau_id_resolve/actions/workflows/ci.yml/badge.svg)](https://github.com/pixelx-jp/plateau_id_resolve/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](pyproject.toml)
[![Yodo Labs](https://img.shields.io/badge/by-Yodo%20Labs-111111.svg)](https://yodolabs.jp)

<picture>
  <img alt="gml:id changes every PLATEAU release; the surrogate_id never does. The same physical building is recognised across versions by geometry, with a match confidence, and cross-version new/demolished/split/merged/modified changes are tracked as honest events."
       src="assets/hero.svg" width="100%" />
</picture>

*"Your records are only as durable as the IDs they're pinned to."*

</div>

---

## The problem

PLATEAU's documentation states plainly that `gml:id` is **not guaranteed to persist across releases**: the same physical building can carry a completely different `gml:id` from one year's CityGML to the next. The `建物ID` (`市区町村コード-bldg-連番`, e.g. `01100-bldg-344507`) is more stable in *format* but is **also not guaranteed persistent**, and is assigned per-municipality in sequence rather than as a global GUID.

So every system that pins records — damage state, inspection history, hazards — to a building id **silently breaks the moment the map updates**: the ids drift and the history no longer lines up with the building it describes. This is the **#1 shared technical blocker** of the whole Physical AI Record Layer stack, and nobody (MLIT included) has solved it cleanly.

## What this does

`plateau-id-resolve` recognises the **same physical building across releases** by geometry (footprint overlap, centroid, area, topology) and mints a permanent **`surrogate_id`** that never changes. It keeps the full history `surrogate ↔ {year: gml_id, 建物ID}`, detects cross-version **新築 / 滅失 / 分割 / 合併 / 増改築**, and bridges **建物ID ↔ 不動産ID** — always with a **match confidence**, and **never pretending an ambiguous split/merge/rebuild is a clean 1:1**.

> This is a **foundation layer** ([role in the stack](#where-this-fits)). Everyone who uses PLATEAU across years hits this pit — so it is independently useful on its own.

## Install

```bash
pip install plateau-id-resolve
# optional extras
pip install 'plateau-id-resolve[geoparquet]'   # read plateau-bridge GeoParquet
pip install 'plateau-id-resolve[http]'         # the HTTP API server
```

Core depends only on **shapely**. The registry is **SQLite (stdlib)** — one file, no server.

## Quickstart (library)

```python
from plateau_id_resolve import Resolver, read_footprints

r = Resolver("city.db")                       # a SQLite file (or ":memory:")
r.ingest_version("2020", read_footprints("city_2020.geojson"))
r.ingest_version("2023", read_footprints("city_2023.geojson"))   # gml:ids all changed

# A record pinned to the surrogate survives the gml:id churn:
res = r.resolve({"gml_id": "bldg_2023_x99"})
print(res.surrogate_id)        # sg_01jh...  (stable, permanent)
print(res.match_confidence)    # 1.0  (exact id lookup)
for h in res.history:
    print(h.dataset_year, h.gml_id, h.building_id, h.match_confidence)

# What changed between releases — honestly:
for e in r.changes(from_year="2020", to_year="2023"):
    print(e.kind, e.surrogate_id, e.confidence, e.related_surrogate_ids)
```

A complete, no-download runnable demo: [`examples/quickstart.py`](examples/quickstart.py).

### Resolve by anything

```python
r.resolve({"gml_id": "..."})              # a release's gml:id
r.resolve({"building_id": "01100-bldg-1"})# 建物ID
r.resolve({"surrogate_id": "sg_..."})     # the persistent id itself
r.resolve({"lat": 35.68, "lon": 139.76})  # a coordinate (point-in-polygon)
r.resolve({"footprint": "POLYGON((...))"})# a footprint (WKT / WKB-hex / GeoJSON)
# Unknown? You get None — never a fabricated id.
```

## CLI

```bash
plateau-id-resolve ingest  --db city.db --year 2020 city_2020.geojson
plateau-id-resolve ingest  --db city.db --year 2023 city_2023.parquet
plateau-id-resolve resolve --db city.db --gml-id bldg_2023_x99
plateau-id-resolve resolve --db city.db --lat 35.68 --lon 139.76
plateau-id-resolve changes --db city.db --from-year 2020 --to-year 2023
plateau-id-resolve bridge  --db city.db --csv crosswalk.csv     # 建物ID -> 不動産ID
plateau-id-resolve stats   --db city.db
```

All output is JSON on stdout, so it composes with `jq`.

## HTTP API (optional)

```bash
pip install 'plateau-id-resolve[http]'
plateau-id-resolve serve --db city.db        # http://127.0.0.1:8000
```

`GET /resolve?gml_id=…` · `GET /changes?from_year=…&to_year=…&bbox=…` · `GET /stats`. An unresolved reference returns **404** — not a fabricated id.

## 建物ID ↔ 不動産ID bridge

PLATEAU's official 不動産ID matching is **probabilistic and partial** (≈7–69 % by city). We don't re-implement it and we don't fabricate a 1:1 mapping — if a backend has no answer, the 不動産ID is **`null` / `covered=false` = unknown**.

```python
from plateau_id_resolve import TableBridge, bridge_all
backend = TableBridge.from_csv("crosswalk.csv")    # building_id,real_estate_id,confidence
coverage = bridge_all(r.registry, backend)
print(coverage.to_dict())   # honest: total / bridgeable / covered / coverage_ratio
```

Plug the official WebAPI (or a DB) via `CallableBridge(fn)` without this package taking a network dependency. For the two official MLIT systems specifically — **RealEstateID-matching-system** (batch/file) and **Building-matching-WebAPI** (online) — see the copy-me, runnable reference [`examples/official_bridge_adapter.py`](examples/official_bridge_adapter.py).

## The honesty invariant

This package is part of a stack with one shared rule: *unsurveyed = unknown, never pretend it's safe* (`covered=false ⇒ value=null`). Here that means:

- a **`match_confidence` is always present** — split/merge/rebuild are genuinely ambiguous, so we expose the uncertainty instead of forcing a 1:1;
- a surrogate id is **permanent and never reused**;
- an unknown 不動産ID is **`null`, never guessed**;
- `resolve()` returns **`None`** rather than inventing a building.

See [`docs/matching.md`](docs/matching.md) and [`docs/confidence.md`](docs/confidence.md) for the full method.

## Data inputs

Footprints come as **GeoJSON** or **GeoParquet** (the latter is what `plateau-bridge` publishes). Extracting footprints from raw CityGML is `plateau-bridge`'s job — this package consumes footprints, not CityGML. You need ≥2 release years to do anything cross-version.

## Where this fits

Part of the **Physical AI Record Layer** open-source ecosystem. This repo implements the **`resolve()` contract (§③)** that the other repos bind to:

```
🧱 plateau-bridge (records + honesty/query core)   🧱 plateau-id-resolve (this repo — persistent IDs)
🚩 plateau-triage (damage state)   robo-permit (deployment rules)
🔌 caveat-mcp     🤖 sixth-sense     🏛 terra-incognita (benchmark)
```

`plateau-triage` / `robo-permit` / `terra-incognita` / `caveat-mcp` all bind their records to the **surrogate_id** this package mints, so history survives map updates.

## Development

```bash
pip install -e '.[dev]'
pytest                       # full test suite
ruff check . && mypy plateau_id_resolve
python examples/quickstart.py
```

## Contributing & contact

Issues and PRs welcome. The contracts (`record.schema.json`, `resolve()`) follow **add-only, minor-bump** versioning — backward compatibility is a discipline, not an aspiration.

- 📧 [pan@yodolabs.jp](mailto:pan@yodolabs.jp)
- 🌐 [yodolabs.jp](https://yodolabs.jp)

## License

[MIT](LICENSE) © 2026 **PixelX Inc. (ピクセルエックス株式会社) / Yodo Labs**

<div align="center">
<sub>Built by <a href="https://yodolabs.jp">Yodo Labs</a> · PixelX Inc. · ピクセルエックス株式会社</sub>
</div>
