# OSM Persistent IDs Specification v.05
_A specification draft of OSM Persistent IDs based on the blog post "Persisting your ids…" by Simon Poole [1], compiled by Stefan Keller._
***NOTE: See the successor [spec. "OSM Persistent IDs Specification v.06-pre"](https://md.coredump.ch/s/qHHUl6KRJ).***
<details>
<summary style="font-weight: bold; color: #FF0000;"> Historical Notes by Stefan</summary>
OSMPID is the abbreviation used here: Shortlist was OSMPID, OOID, OSMPI, POID, ODMLNK, OPI, MAPID, OLID. Dismissed: OPID, OOID Clashes with OI from Esri)
</details>
## 1. Purpose
An **OSM Persistent ID** (OSMPID) provides a stable (permanent) reference to an OpenStreetMap object, even when updates are made. This includes cases where the top-level object's version does not change, but one or more of its child elements do.
The goal is to establish an object identity service that can detect changes in the semantic content of a referenced OSM object since its original recording using only current OSM data.
## 2. Source-Local, Persistent, and Global IDs
A **source-local ID** (or technical ID) is an identifier used by a specific system to identify one of its own records. In OSM, `node/2641352539` is a source-local ID: it identifies a particular OSM element of type `node` with the local numeric ID `2641352539`. Source-local IDs are implementation-specific; they are often numeric in whole or in part, but do not need to be numeric. In OSM, the local IDs of `node`, `way`, and `relation` elements are numeric.
A **global ID** is an identifier intended to work across multiple data sources, not only OSM. A global ID would be distributed and source-independent, like a Digital Object Identifier (DOI) [2], allowing the same real-world entity to be referenced across OSM and other datasets.
A **persistent (permanent) ID** is an identifier designed to permanently keep referring to the same real-world entity according to a chosen persistence model. Whether an entity is still "the same" depends on application logic and use case. For example, a restaurant review site may want one persistent (and permanent) ID to continue to refer to a restaurant despite small edits in OSM, but may decide that a sufficiently large change should result in a different persistent (and permanent) ID.
## 3. OSM Persistent ID Specification
This specification defines such a persistent ID for OSM-backed entities by combining the source-local object reference with version and retrieval timestamp semantics.
### 3.1 Data Structure
A persistent reference to an OSM object **must** store the following components, with main parts separated by a semicolon `;`:
1. `element_type` — the OSM element type `node`, `way`, or `relation` (mandatory)
1. `element_id` — the OSM object ID (mandatory).
1. `element_version` — the version of the object at the time of retrieval, prefixed with `@v` and appended directly to `element_id` without a separator (mandatory).
1. `retrieval_timestamp` — either the timestamp of the object or that of the youngest child, or the timestamp when the object was retrieved and the mapping was created (mandatory).
1. `key` — the key if the OSM element has multiple "top level" keys (optional).
The full string format is:
```
element_type/element_id@vN;retrieval_timestamp[;entity_type]
```
where `N` is the version integer, `retrieval_timestamp` is an ISO 8601 UTC datetime, and `entity_type` is the optional key qualifier.
This tuple forms the persistent identifier data structure. It is of type string with an ASCII character set and with a length between 38 and 75 characters, with an average of approximately 45.
:::info
**INFO:** The type of this ID is a string. However, there are cases where a numeric form is required. There are several ways to encode the identifier into a pure (big) integer representation (see e.g., [3]).
:::
### 3.2 Semantics
A persistent ID refers not only to the top-level OSM element, but also to the state of its referenced child elements as of the recorded `retrieval_timestamp`.
The `retrieval_timestamp` should be determined by recursively iterating over the child elements and the parent and choosing the most recent value; if that is not possible, the time of retrieval can be used as a proxy.
For a **way**, child elements are its member nodes. For a **relation**, child elements are its members, which may themselves be nodes, ways, or relations. For a **node**, there are no child elements.
An object is considered **changed** if the top-level object version has changed, or if any recursively referenced child element has a timestamp more recent than the stored `retrieval_timestamp`.
OSM elements can carry attributes ("tags") for multiple real world objects as long as the "top level" keys do not conflict; the optional `entity_type` allows disambiguating between these.
### 3.3 Examples
Examples of OSM persistent IDs (tbd.):
* `node/2641352539@v7;2026-03-12T10:15:00Z;amenity`
* `way/99887766@v12;2026-03-10T08:00:00Z;amenity`
* `relation/445566@v3;2026-03-01T14:30:00Z`
## 4. OSMPID Handling
To determine whether an OSMPID still matches the current OSM object state, the current object identified by `element_type` and `element_id` must be retrieved and its current version compared with the stored `element_version`. If the current version differs, the object has changed.
If the current version is unchanged, the object's child elements must be inspected recursively. For each child element, its current timestamp must be compared with the stored `retrieval_timestamp`. If any child element has a timestamp later than the stored `retrieval_timestamp`, the object has changed. If no child element is newer, the object is considered unchanged.
The recursive check must traverse the full dependency tree of the object. A node is a terminal element and has no recursion. A way requires recursion into its member nodes. A relation requires recursion into all members, and if a member is a way or relation, recursion must continue further. Implementations should protect against cycles in relations by tracking visited elements.
This mechanism relies only on current OSM data. It does not require historical snapshots, diff history, attic data, or any other non-standard or exotic storage.
## 5. OSMPID Service
**Description**: A lightweight HTTP endpoint that validates whether an OSM object — including all its child elements — is semantically unchanged since it was originally recorded. It accepts a single OSMPID string as input and returns a standard HTTP status code, requiring no historical snapshots or diff data. The endpoint is inspired by HTTP conditional requests but implements custom validation logic — the `retrieval_timestamp` is embedded in the OSMPID itself, not passed as an `If-Modified-Since` header.
**Input:** A single OSMPID string, passed as a percent-encoded query parameter, e.g.:
```
GET /osmpid?id=node%2F2641352539%40v7%3B2026-03-12T10%3A15%3A00Z%3Bamenity
```
Decoded: `node/2641352539@v7;2026-03-12T10:15:00Z;amenity`
The handler parses this into:
| Component | Example Value | Usage |
|---|---|---|
| `element_type` | `node` | Determines recursion depth |
| `element_id` | `2641352539` | Lookup in OSM DB |
| `element_version` | `v7` | Compared with current version |
| `retrieval_timestamp` | `2026-03-12T10:15:00Z` | Threshold for child element check |
| `key` | `amenity=restaurant` | Tag presence check |
**Processing:**
1. Fetches the current object using `element_type` + `element_id`
2. Checks whether `element_version` still matches
3. If yes: recursively checks whether any child element has a timestamp newer than `retrieval_timestamp`
4. Checks whether the object still carries the expected tag key or value (e.g. `amenity`) — if absent, the object is considered semantically changed → `200`
**Output:**
| Result | HTTP Status | Reason |
|---|---|---|
| Unchanged | `304 Not Modified` | Version, child elements, and tag all unchanged |
| Changed | `200 OK` | Version, child element or tag has changed; new OSMPID in response body |
| Deleted | `410 Gone` | Object no longer exists |
| Unknown | `404 Not Found` | Object was never found |
| Malformed ID | `422 Unprocessable Entity` | OSMPID string could not be parsed |
The response always includes a `Last-Modified` header with the computed max-timestamp over the object and all its child elements — which serves as the new `retrieval_timestamp` for an updated OSMPID.
**Response Bodies (JSON):**
The `304 Not Modified` response carries no body, consistent with HTTP semantics. All other responses return a JSON body as follows.
```json
// 200 OK — object has changed; body contains an updated OSMPID
{
"status": "changed",
"permanent_id": "node/2641352539@v8;2026-03-28T14:22:00Z;amenity",
"element_type": "node",
"element_id": 2641352539,
"current_version": 8,
"max_child_timestamp": "2026-03-28T14:22:00Z",
"changed_reason": "version"
}
```
The `changed_reason` field contains one of three values: `"version"` if the top-level version number changed, `"child_timestamp"` if a child element was updated while the top-level version remained the same, or `"tag"` if the expected tag key or value is no longer present.
```json
// 410 Gone — object has been deleted from OSM
{
"status": "deleted",
"element_type": "node",
"element_id": 2641352539
}
```
```json
// 404 Not Found — object was never found in OSM
{
"status": "not_found",
"element_type": "node",
"element_id": 2641352539
}
```
```json
// 422 Unprocessable Entity — OSMPID string could not be parsed
{
"status": "malformed",
"detail": "Could not parse element_version from token 'v'",
"received": "node/2641352539@v;2026-03-12T10:15:00Z"
}
```
For the `200` response, the `Last-Modified` header is set to `max_child_timestamp`, redundant with the body field but included for HTTP cache compatibility.
## APPENDIX: Implementation Notes
The implementation could be written in Python and rely on five core libraries: pyosmium imports OSM PBF files and manages incremental replication updates. psycopg3 provides fast asynchronous access to PostgreSQL. Pydantic parses and validates the incoming OSMPID string. FastAPI handles the HTTP endpoint, paired with Uvicorn as the async server.
For performance, `max_child_timestamp` on the parent element can eventually be precomputed at import/update time.
The expected performance ceiling is the network latency of the provider (Hetzner) and FastAPI/Uvicorn request handling, not the database.
## APPENDIX: Bibliography
[1]: https://www.openstreetmap.org/user/SimonPoole/diary/408332 "Persisting your ids…" by Simon Poole on 8 March 2026 (OSM Diary).
[2]: https://doi.org/10.1000/182 DOI Foundation. (2025). DOI Handbook.
[3]: https://www.giswiki.ch/Permanent_ID_for_OSM "Permanent ID for OSM – Geometa Lab OST"