OpenStreetMap Contribution Design¶
Status: Accepted implementation contract for issue #124
Last reviewed: 2026-07-30
This document defines how Book Corners may prepare and manually contribute a user-submitted public bookcase to OpenStreetMap (OSM). It defines future work; it does not enable or perform an OSM write.
Decision summary¶
- Permission is an unchecked, per-submission boolean. It defaults to
false. - Existing records and clients that omit the field are not opted in.
- There is no account-level OSM preference or dashboard setting.
- A true value permits later review. It never submits, queues, schedules, or guarantees an OSM contribution.
- The public library API does not expose the permission. The create response and the authenticated submitter's contribution response do.
- Later owner edits cannot enable or change the permission.
- Only approved, directly user-submitted, opted-in libraries with no unresolved update, duplicate, or previous contribution can become candidates.
- OSM imports, legacy records, staff-created records, ownerless records, and records with unknown provenance are ineligible.
- Every write is initiated and confirmed by an authorized administrator after a fresh duplicate check and exact tag preview.
- A dedicated OSM import account and an OAuth 2.0 token with only
write_apipermission are used. Book Corners never collects a submitter's OSM credentials. - Account details, private contacts, photos, AI-generated text, and internal database identifiers are never sent to OSM.
- Production writes remain disabled until the OSM import/automated-edit review, local community consultation, and data-licensing gates in this document are complete.
Submission permission and provenance¶
Library fields¶
The existing Library row is the submission record. Add these fields in
issue #126:
| Field | Type and default | Purpose |
|---|---|---|
osm_submission_allowed |
BooleanField(default=False) |
The submitter permitted later manual OSM review for this submission. Do not add a database index to this low-cardinality field. |
osm_submission_allowed_at |
nullable timestamp | When a true permission was recorded. It remains null when the value is false or omitted. |
submission_origin |
indexed text choice, default legacy |
Durable provenance with legacy, user, staff, and import values. |
The data migration must set every existing row to
osm_submission_allowed=false, osm_submission_allowed_at=null, and
submission_origin=legacy. It must not infer consent or direct submission from
created_by, because the GeoJSON importer also records a staff user in that
field.
New records set provenance as follows:
- Authenticated web and API creates set
submission_origin=user. - Django admin creates set
submission_origin=staff. - GeoJSON and future bulk imports set
submission_origin=import. - No import or staff workflow accepts
osm_submission_allowed=true.
source and external_id remain data-origin metadata. They are additional
safety signals, not substitutes for submission_origin.
API contract¶
POST /api/v1/libraries/ accepts this optional multipart form field:
| Field | Type | Default | Meaning |
|---|---|---|---|
osm_submission_allowed |
boolean | false |
Permit an administrator to consider this submission for a later manual OSM contribution. |
The server stores the value only after authenticated validation succeeds. A true value has no external side effect.
The 201 Created response uses a submission-specific response schema that
extends the existing library response with osm_submission_allowed. The same
field is added to each item returned by authenticated
GET /api/v1/libraries/mine, so a client can confirm the stored choice after
reconnecting.
The field is intentionally absent from public list and detail responses, favourites, statistics, and unauthenticated schemas. It is permission metadata, not a public property of a bookcase.
PATCH /api/v1/libraries/{slug} does not accept this field. An attempted update
is rejected as an unsupported field and does not change the stored value. Owner
edits, including staged edits to approved libraries, preserve the original
submission permission.
If a submitter asks to withdraw permission before contribution, staff may only
change true to false; staff cannot grant permission on the submitter's
behalf. The withdrawal and actor are recorded in the audit model described
below. The field is read-only in the generic Django admin; withdrawal uses a
dedicated action that enforces this direction. Enabling permission after the
original submission is outside this rollout and would require a new explicit
user-facing consent flow.
Eligibility¶
A library is eligible for an OSM preview only when every condition below is true:
status=approved.submission_origin=user.created_bystill references the authenticated submitter.osm_submission_allowed=true.sourcedoes not identify OpenStreetMap andexternal_iddoes not contain an OSM element identifier.- No staged
pending_changesorpending_photoawaits moderation. - No OSM element has already been recorded for the library.
- The latest duplicate check is successful, has no unresolved possible duplicate, and is recent enough for the final confirmation.
- The global policy, licensing, credentials, and production feature gates are enabled.
The same predicate is used for admin filters, the preview page, and the transaction immediately before a write. The write path must re-read the row with a database lock and re-evaluate the predicate; it must not trust a stale admin page.
Deleting an account sets created_by to null under the existing model
relationship and therefore makes an unsubmitted library ineligible. Approval or
opt-in alone is never sufficient.
OSM state and audit records¶
Add a one-to-one OpenStreetMapContribution model in
issue #127. It stores
the current operational state without overloading Library.source.
| Field | Purpose |
|---|---|
library |
One-to-one link to Library. |
status |
Indexed choice: unchecked, no_match, possible_duplicate, already_present, submitting, contributed, or failed. |
checked_at |
Time of the most recent completed duplicate check. |
duplicate_candidates |
Sanitized JSON list of OSM element type, ID, distance, relevant tags, and match signals. |
osm_element_type |
node, way, or relation; the initial create path only creates a node. |
osm_element_id |
OSM element ID when an existing or created feature is confirmed. |
changeset_id |
OSM changeset used for the contribution. |
contributed_at |
Time Book Corners confirmed the OSM result. |
contributed_by |
Staff user who confirmed the write. |
last_attempt_key |
Unique UUID used to serialize and reconcile one write attempt. |
last_error_code |
Stable internal error category without credentials or response bodies. |
last_error_message |
Sanitized operator-facing failure detail. |
Add a conditional uniqueness constraint for non-null
(osm_element_type, osm_element_id). One Book Corners library may link to one
OSM feature, and the same OSM feature cannot satisfy two Book Corners
contributions.
Add an append-only OpenStreetMapContributionEvent model. Each event stores the
library/contribution, event type, outcome, staff actor when applicable,
timestamp, attempt UUID, and sanitized JSON details. Event types cover:
- permission withdrawal;
- duplicate check and its candidate snapshot;
- duplicate resolution with the staff reason;
- write started with the exact outgoing coordinate, tags, and changeset tags;
- write succeeded with the OSM element and changeset IDs;
- write failed or required reconciliation.
The event table is the audit history; the one-to-one record is the current filterable state. Tokens, client secrets, private contacts, photos, and full HTTP headers or bodies must never enter either model or application logs.
Duplicate detection and conflation¶
The existing find_duplicates command detects Book Corners records by
normalized address and by same-street proximity within 100 metres. OSM
conflation is a separate check against current OSM data and must not reuse a
cached Book Corners-only result.
Candidate query¶
For a manual preview, query a configured Overpass-compatible endpoint for nodes, ways, and relations within 100 metres of the submitted point. Request:
- every
amenity=public_bookcasefeature; - nearby features with
amenity=library,amenity=give_box, orshop=booksfor secondary warnings; and - the tags needed to compare
name, address,brand, andoperator.
The checker calculates distance to nodes and to the representative point of ways and relations. It stores only the element type, element ID, distance, relevant public tags, and triggered signals.
Match signals¶
A result becomes a blocking possible_duplicate when any condition is true:
- an
amenity=public_bookcaseis within 25 metres; - an
amenity=public_bookcasewithin 100 metres has the same normalized name; - an
amenity=public_bookcasewithin 100 metres has the same normalized street and house number; - a feature within 100 metres has matching normalized
brandoroperatortogether with a compatible book-sharing tag; or - its OSM element ID is already linked to any Book Corners library.
Normalization trims whitespace, case-folds Unicode, removes punctuation for comparison, and compares address components only when both sides contain them. Missing tags never count as proof that no duplicate exists.
Other returned features are warnings in the preview. An authorized administrator must still inspect the map and tags before confirming no match.
Resolution and freshness¶
A possible duplicate blocks creation. The administrator must choose one of these recorded outcomes:
- Same feature: link the existing OSM element, mark the contribution
already_present, and do not create anything. - Different feature: record a reason explaining the distinction. A fresh duplicate check is still required immediately before creation.
- Unclear: leave
possible_duplicateand stop.
A failed query, rate limit, timeout, malformed response, or check older than 15 minutes blocks the write. The final confirmation always reruns the check; cached results may render the preview but cannot authorize a write.
Supported OSM mapping¶
The initial integration creates only a node at the approved, reviewed
Library.location. It does not modify existing OSM geometry.
| Book Corners value | OSM output | Rule |
|---|---|---|
| Library type | amenity=public_bookcase |
Always present. |
name |
name=* |
Only when staff verifies it is a real on-site proper name. Never send an AI-generated or generic display name. |
capacity |
capacity=* |
Positive integer only. |
is_indoor=true |
location=indoor |
Omit for false or unknown. |
wheelchair_accessible |
wheelchair=yes, no, or limited |
Map only the existing explicit values. |
is_lit |
lit=yes or lit=no |
Omit when unknown. |
brand |
brand=* |
Only when visible or otherwise independently verified. |
operator |
operator=* |
Only when it is the public operator of the physical bookcase. |
website |
website=* |
Only when it is a public page specifically for the bookcase or its operator. |
The admin preview starts with this allowlist and shows the exact final tags. Unsupported fields cannot be added through free-form admin input.
The initial mapping deliberately excludes:
description, because it may contain AI-generated or subjective text;- combined Book Corners
address, city, country, and postal code, because the current model cannot reliably produce a structured OSM address; contactand every account, email, phone, or private-contact field;- Book Corners photos, thumbnails, captions, and media URLs;
- Book Corners IDs, slugs, moderation state, timestamps, and internal source metadata;
sourceorexternal_idas element tags; and- fields copied from an incompatible third-party map or dataset.
Adding another object tag or creating areas, ways, or relations changes the consulted import scope and requires a new design review and, where applicable, renewed OSM community approval.
Changesets and API workflow¶
Every library is checked and confirmed individually. A one-off confirmation may use a one-node changeset, but the system must not create a stream of one-object changesets from a bulk selection. Repeated confirmations in one operator session must be grouped by a small geographic area using a reviewed batch and one changeset. A changeset never spans unrelated cities or regions. The final grouping thresholds are recorded in the consulted OSM plan; changing them later requires renewed consultation.
The changeset includes:
| Tag | Value |
|---|---|
comment |
Human-readable text such as Add a reviewed public bookcase in Florence. |
created_by |
Book Corners/<deployed version>. |
source |
The source wording approved during import consultation, expected to identify an individually reviewed Book Corners user submission. |
description |
Public OSM wiki URL documenting the import/automated-edit plan and contact route. |
mechanical |
yes, unless the recorded OSM consultation explicitly requires a different classification. |
review_requested |
yes during the initial monitored rollout. |
Attribution is carried by the approved source value, the linked wiki plan, the
dedicated account profile, and an OSM Contributors wiki entry if the consultation
requires one. If the approved data permission has a concise licence identifier,
record it as source:license on the changeset. Do not add attribution, licence,
or source metadata to the public-bookcase element itself unless the OSM
consultation specifically requires it.
The implementation uses the OSM API v0.6 workflow:
- Revalidate eligibility and acquire the local write lock.
- Record the
write_startedaudit event and setstatus=submitting. - Create the changeset with
PUT /api/0.6/changeset/createand immediately persist its ID. - Create the reviewed node with
POST /api/0.6/nodes, or use the diff upload endpoint for a reviewed same-area batch. - Persist the returned node ID, close the changeset, and record success.
- If closing fails after the node was created, preserve the successful element ID and reconcile the changeset rather than creating another node.
OSM changesets are not atomic. If a create response is lost or ambiguous, the system must download the recorded changeset and reconcile its contents before offering a retry. It must never blindly retry an OSM write.
Authentication and editing account¶
Use a dedicated import account following the OSM naming convention, for example
BookCorners_Import. Its public profile links to:
- the responsible Book Corners project/account;
- the published import or automated-edit plan;
- a monitored contact route; and
- the relevant consultation record.
The account accepts the OSM contributor terms and is used only for this documented workflow. Staff members do not contribute through personal OSM accounts, and submitters are never asked for OSM access.
Register Book Corners as a confidential OAuth 2.0 application. Authorize the
dedicated account once with only the write_api scope. Do not request profile,
preferences, GPX, notes, diary, messaging, or other unrelated scopes.
The runtime sends the bearer token in the authorization header and verifies
allow_write_api through GET /api/0.6/permissions before enabling the admin
action. OSM access tokens do not currently expire automatically, but they can be
revoked; a missing permission or 401 response disables writes until an
operator rotates the token. The development OSM server uses a separate account,
application, and token because it does not share production data.
Policy and licensing gates¶
Book Corners must treat this workflow as an external-data import and a script-assisted edit unless the OSM community explicitly records a different classification. Individual admin review does not waive the import requirements.
Before the first production write:
- Publish an OSM wiki plan covering the dataset, rights, field mapping, conflation, software source, account, changeset tags, quality assurance, rollback, frequency, and community opt-out route.
- Discuss the plan on the OSM Community Forum and with the appropriate local communities. Do not write until at least the documented import-review period has passed and concerns are resolved.
- Record confirmation that Book Corners may release the submitted factual data under terms compatible with OSM's ODbL. A privacy notice or checkbox that merely permits submission is not, by itself, a data licence.
- Make the frontend consent copy state that the submitter supplied factual information they are entitled to share, that it may be published in OSM under OSM's terms, and that it was not copied from an incompatible source such as Google Maps.
- Publish the exact source attribution accepted during consultation and use it consistently in the wiki plan, account profile, and changesets.
- Document how Book Corners will stop, investigate, and if necessary revert its changes in response to community concerns or opt-out requests.
These are hard launch gates. If licensing or community approval is uncertain, the feature remains disabled even when a submitter opted in.
The rollback procedure is manual: disable writes, preserve the affected changeset and audit records, notify the consulted community, and use an experienced OSM mapper or the Data Working Group to plan a revert. Book Corners must not run an automatic inverse edit because later mapper changes can make a blind rollback destructive.
Configuration, credentials, and service limits¶
All settings are environment configuration. Suggested names are:
| Setting | Requirement |
|---|---|
OSM_WRITE_ENABLED |
Defaults to false. It is the production kill switch. |
OSM_API_BASE_URL |
Required when enabled. Production and development URLs must be explicitly selected. |
OSM_OAUTH_ACCESS_TOKEN |
Secret bearer token for the dedicated account. |
OSM_OVERPASS_URL |
Configured duplicate-query provider. |
OSM_USER_AGENT |
Required application/version plus a monitored contact URL. |
OSM_PLAN_URL |
Published OSM wiki plan used in changeset metadata. |
OSM_DUPLICATE_RADIUS_METERS |
Defaults to 100. |
OSM_DUPLICATE_CHECK_MAX_AGE_SECONDS |
Defaults to 900 (15 minutes). |
Store secrets through Dokku configuration, following the existing production secret-management pattern. Do not store the bearer token in the database, repository, admin forms, audit JSON, or logs. OAuth client credentials used for manual token rotation are operator secrets and need not be available to the web process.
OSM does not promise a fixed request allowance for this use case. The client must:
- send a valid, contactable User-Agent containing the application version;
- query API capabilities rather than hard-code server element limits;
- keep this workflow manual and low-volume with one in-flight write;
- set bounded connect and response timeouts;
- respect
Retry-Afterfor429responses; - retry read-only duplicate checks at most three times with bounded exponential backoff; and
- never automatically retry a create, update, or delete request with an ambiguous outcome.
Duplicate-check failures remain visible and block contribution. Write failures record a sanitized stable error, preserve all known OSM IDs, and require reconciliation before an administrator can retry.
Deployment boundaries¶
Deliver the epic in this order:
- #126: add default-off permission and provenance, API input, private responses, migrations, and tests. No OSM client or external side effect.
- #127: add state/audit models, duplicate checks, admin filters, warnings, and resolution. No OSM write.
- #125: add the exact preview, confirmation, OAuth client, reconciliation,
and manual write action behind
OSM_WRITE_ENABLED=false. - Complete the policy, licensing, account, credentials, development-server, and community gates.
- Enable the web and iOS opt-in interfaces only after steps 1-4 are safely deployed. Existing and omitted values remain false, so backend-first deployment creates no candidates.
- Perform a small, monitored production rollout. Keep the kill switch available and review every early changeset and any community feedback.
There is no task queue or periodic job for OSM contribution. Background AI enrichment, moderation approval, and receipt of a true API field must never call the OSM client.
Verification required by implementation tickets¶
Automated tests must use a fake OSM transport and prove:
- omitted and explicit false permissions remain ineligible;
- different submissions by one user store independent values;
- legacy, imported, staff, ownerless, rejected, unapproved, and pending-update records are blocked;
- public responses do not expose permission;
- owner edits cannot enable or change permission;
- duplicate signals, stale checks, failed checks, and unresolved candidates block writes;
- permissions, provenance, approval, duplicate state, and prior contribution are rechecked inside the write lock;
- exact node and changeset payloads contain only allowlisted values;
- private contacts, photos, account data, and AI-generated text never enter payloads, audit data, or logs;
- concurrent or repeated actions cannot create a second feature;
- ambiguous outcomes require reconciliation rather than retry; and
- no test reaches the production OSM service.
Manual pre-production verification uses
https://master.apis.dev.openstreetmap.org with its separate account and token.
The operator must inspect the created node, changeset tags, audit history,
duplicate preview, error handling, and kill switch before production can be
considered.