WEP 8 GeoPlace Extensions & Amenity Import¶
Extend GeoPlace with typed detail models and import amenities from OSM.
Motivation¶
GeoPlace currently covers huts and natural features, but lacks structured data
for amenities relevant to Alpine tours: food supplies, transport, emergency
services, and accommodation. This WEP introduces typed detail models, per-source
import policies, and an automated OSM import pipeline.
GeoPlace Extensions¶
Rather than adding all fields to GeoPlace directly, we introduce lightweight
OneToOne detail models per place type. GeoPlace itself gains a few shared
fields.
New fields on GeoPlace
| Field | Description |
|---|---|
slug |
Unique URL identifier |
description |
Long-form text (i18n) |
review_status |
Editorial state: new / review / done / work / reject |
review_comment |
Internal reviewer note |
detail_type |
Which detail model is attached: amenity / transport / admin / natural / none |
protected_fields |
JSON list of field names no source may overwrite |
shape |
Optional polygon geometry for natural features and administrative areas |
osm_tags |
Raw tags from OpenStreetMap (JSON) |
extra |
Category-specific overflow data (JSON) |
websites |
List of URLs with optional labels (shared across all place types) |
protected_fields is maintained automatically — whenever a field is edited via
the Wodore admin or API, its name is appended to the list. Falls back to a
minimal global default of ["name", "location"] when empty. Also editable
manually in the admin.
osm_tags stores the raw OpenStreetMap tag data for each place, preserving
the complete source information. This is useful for debugging, data quality
analysis, and future enrichment.
extra provides a flexible overflow field for category-specific data that
doesn't fit into the standard schema. Each category can define its own expected
structure within this JSON field.
websites is a shared field across all place types, storing a list of website
objects with optional labels (e.g., "Official", "Booking", "Menu").
detail_type is a fixed enum tied to the available detail models — not derived
from category. Categories remain flexible (new category slugs can be added
freely). The mapping between a category and its detail_type lives in code.
GeoPlace exposes factory methods (create_amenity, create_transport, …)
that set detail_type and create the corresponding detail row atomically.
Natural features (peaks, passes, lakes, glaciers) have detail_type=natural
but no detail model — the category slug and existing GeoPlace fields
(location, elevation, name, parent) are sufficient. Mountain ranges and
administrative regions are represented via the parent self-FK.
AmenityDetail — food, shops, restaurants, emergency, accommodation
| Field | Description |
|---|---|
operating_status |
open / temporarily_closed / permanently_closed / unknown |
opening_months |
Monthly availability per month: yes / yesish / maybe / no / noish / unknown |
opening_hours |
Structured weekly hours per weekday + public holidays |
phones |
List of phone numbers |
Note: websites has been moved to the base GeoPlace model and is shared
across all place types. The extra field has also been moved to GeoPlace
for broader use.
TransportDetail — bus stops, train stations, cable cars
| Field | Description |
|---|---|
station_id |
External identifier (e.g. Swiss DIDOK, UIC station code) |
operator |
Operating company (e.g. SBB, PostAuto, RhB) |
Connects naturally to GTFS integration (see WEP003).
AdminDetail — cities, villages, valleys
| Field | Description |
|---|---|
admin_level |
OSM admin level (2 = country … 10 = village) - can be calculated from parent but stored for performance |
population |
Inhabitant count |
postal_code |
Postal code for this administrative area |
iso_code |
ISO 3166-2 code for administrative divisions (e.g., CH-ZH for Zürich) |
Note: website has been moved to the base GeoPlace model's websites field
and is shared across all place types.
The admin_level field follows the OpenStreetMap convention:
- Level 2: Country (e.g., Switzerland)
- Level 4: State/Province/Canton (e.g., Zürich)
- Level 6: County/District (e.g., Zürich District)
- Level 8: City/Municipality (e.g., Zürich city)
- Level 10: Village/Hamlet (e.g., Zermatt)
While admin_level can be calculated from the parent relationship, storing it
directly improves performance and preserves the original OSM classification.
Category Hierarchy¶
Categories follow a parent.child slug pattern. The mapping to detail_type
is defined in code and is not a hard DB constraint — categories stay flexible.
Complete Category Taxonomy (15 top-level categories)¶
This taxonomy balances Alpine/tourism focus with general utility, informed by OpenStreetMap, Google Maps, OsmAnd, and Organic Maps conventions.
restaurant (prepared food & drinks) → amenity
- restaurant, cafe, pub (includes bars and biergartens), fast_food, food_court, ice_cream
groceries (food shopping) → amenity
- supermarket, convenience, bakery, butcher, farm, dairy (includes greengrocer, deli, cheese shops), beverages, vending_machine
accommodation (lodging) → accommodation (not implemented yet, use amenity)
- hotel, hostel, guesthouse, campground, alpine_hut
health_and_emergency (medical & urgent response) → amenity
- hospital, clinic, doctor, dentist, pharmacy, optician, fire_station, police, mountain_rescue
transport (public transit) → transport
- bus_stop, train_station, cable_car, gondola, chairlift, funicular
automotive (vehicle services) → amenity
- parking, fuel, charging_station, car_wash, car_rental
sport (activities, facilities, instruction) → amenity
- climbing_gym, swimming_pool, fitness_center, ski_school, playground, mountain_guide, skate_park
outdoor_services (Alpine/outdoor gear - rental, repair, shops) → amenity
- ski_rental, bike_rental, bike_repair, bike_shop, sports_shop (includes outdoor shops)
tourism (sightseeing & information) → amenity
- information (includes info boards, offices, maps), viewpoint, museum, attraction, artwork, memorial, hiking_post (guideposts)
natural (natural features) → natural
- peak, pass, lake, glacier, waterfall, cave, spring, cliff, saddle, ridge, valley_entrance
admin (administrative areas) → admin
- country, state, canton, district, city, municipality, village, hamlet
utilities (public infrastructure) → amenity
- toilets, drinking_water, shower, waste_disposal, picnic_area, firepit, bench
finance (banking) → amenity
- bank, atm Review the code for import improvements? But probably only the full import is slow, diff import should be faster and all I need. But I am still thinking it gets slwoer with more data, this is not good ..
shopping (general retail - non-food) → amenity
- clothes, shoe, hardware, books, electronics, jewelry, toys, gift (includes souvenirs), store (general stores, variety stores, department stores), mall
services (personal services) → amenity
- hairdresser, tailor, computer_repair, veterinary, laundry
Category → detail_type Mapping¶
| Categories | detail_type |
Notes |
|---|---|---|
| restaurant, groceries, health_and_emergency, automotive, outdoor_services, shopping, services, utilities, sport, tourism, finance | amenity |
Uses AmenityDetail |
| accommodation | accommodation |
Uses AccommodationDetail (not implemented yet, use amenity) |
| transport | transport |
Uses TransportDetail |
| natural | natural |
No detail model - base GeoPlace fields sufficient |
| admin | admin |
Uses AdminDetail |
Natural features use existing GeoPlace fields:
location(Point) for peaks, passes, waypointselevationfor altitudeparent(self-FK) for mountain ranges (e.g., peak → Bernese Alps)shape(Polygon) for lakes, glaciers, valleys- Category slug differentiates:
natural.peak,natural.lake, etc.
Admin areas use AdminDetail plus:
location(Point) - administrative centershape(Polygon) - boundaryparent(self-FK) - village → municipality → canton → country- Fields: admin_level, population, postal_code, iso_code
Source Tracking & Import Policy¶
All source-related data lives in GeoPlaceSourceAssociation, extended with:
| New field | Description |
|---|---|
modified_date |
Last time this source updated the record (set on every import run) |
update_policy |
How this source may update the record |
delete_policy |
What happens when this source no longer includes the record |
priority |
Source precedence for field conflicts — lower number wins (e.g. 1=OSM, 2=Overture) |
import_date (already exists) records the first import from a source.
modified_date records the most recent update.
The wodore source is the built-in manual edit marker. Whenever a place is
edited via the Wodore admin or API, two things happen automatically:
wodore.modified_dateis set — replaces the existingis_modifiedboolean- The edited field name is appended to
place.protected_fields
Import commands respect two levels of field protection:
place.protected_fields— fields no source may ever overwrite (manually curated)priority— when two sources both provide the same non-protected field, the lower priority number wins. Higher-priority sources fill fields first; lower-priority sources only fill what remains.
update_policy
| Value | Behaviour |
|---|---|
always |
Always overwrite all fields |
merge |
Skip fields already edited by the wodore source |
protected |
Never overwrite |
auto_protect |
Behaves as merge until wodore.modified_date is set, then switches to protected |
delete_policy
| Value | Behaviour |
|---|---|
deactivate |
Set is_active=False |
keep |
Ignore deletion |
delete |
Hard delete |
auto_keep |
Behaves as deactivate until wodore.modified_date is set, then switches to keep |
Multi-source Deduplication¶
When importing from a new source, each record must be matched against existing
GeoPlace entries before creating a new one. Matching logic lives in each
import script. The shared lookup order is:
- Source + source_id — if this source has already imported this record (association exists), update in place. This ensures manually reviewed records are never re-duplicated on subsequent runs.
- External ID cross-reference — some sources carry IDs from other sources
(e.g. Overture stores
osm_id). If a match is found via another source'ssource_id, associate and update according to policy. - Location + category parent — match within a defined radius and same
category parent slug (e.g.
accommodation). Tolerant of type differences between sources (e.g.hutvsunattended_hut). - Location + very small radius — no type match possible. If one candidate
→ associate. If multiple candidates → set
review_status=reviewon all, keep records separate until manually resolved.
Run order determines effective priority — whichever source runs first creates
the GeoPlace. Subsequent sources associate to it and fill non-protected fields
according to their update_policy and the place's priority ordering.
Staging table (future option)
For higher data quality requirements or frequent re-imports, a lightweight staging table can be introduced between fetch and merge:
- Fetch — import all source records into a staging table (
location+source_dataJSON) - Merge — run the deduplication and upsert logic against
GeoPlace - Cleanup — delete staging rows, or keep for diff tracking and review
This separates the raw import from the merge decision, makes diffs between runs
easy to compute, and allows a review step before anything touches GeoPlace.
Currently used for huts. For lower-stakes places (bakeries, bus stops) the
direct upsert approach is sufficient — staging can be introduced per source if
conflict rates or data quality requirements justify it.
Data Import¶
GeoPlace and its detail models are populated via Django management commands,
one per source. Import logic is source-agnostic at the model level — factory
methods (create_amenity, create_transport, …) are reused regardless of source.
Import run
- Upsert — iterate all records from the source. For each record, check
update_policyon the association and create or updateGeoPlace+ detail model accordingly. Setmodified_dateon the association. - Cleanup — after the upsert pass, find all associations for this source
where
modified_dateis older than the current run (i.e. not seen in this import). Applydelete_policy: deactivate, hard delete, or keep as-is.
This two-pass approach means every import is a full sync — no need to track diffs externally.
Sources
- OSM (primary for amenities) — weekly CronJob fetching Alps PBF from
Geofabrik, filtered with
osmium-tool, parsed withpyosmium. Good rural and Alpine coverage, includesopening_hours. Upsert key:(osm_id, osm_type). - GeoNames (currently implemented) — natural features and admin places.
- Overture Maps (future) — potential supplement for places with low OSM coverage.
API¶
Each detail_type gets its own endpoint with a fixed, fully-typed response
shape. geo/places/{id} always returns base fields only — no nullable detail
blobs, no discriminated unions.
| Endpoint | Response | Notes |
|---|---|---|
geo/places/search |
Base fields, paginated | Existing endpoint |
geo/places/{id} |
Base GeoPlace fields |
Lightweight, all types |
geo/amenity/{id} |
Base + AmenityDetail |
Food, shops, emergency, accommodation |
geo/transport/{id} |
Base + TransportDetail |
Bus stops, stations, cable cars |
geo/admin/{id} |
Base + AdminDetail |
Cities, villages |
geo/natural/{id} |
Base fields | Same as places for now, reserved for future |
detail_type on the base response tells the client which typed endpoint to
call for full details.
Map layers (Martin)
For vector tile serving via Martin, a PostgreSQL view is created per logical
map layer (e.g. v_layer_food_supply, v_layer_transport,
v_layer_emergency). Each view joins GeoPlace with the relevant detail model
and exposes only the fields needed for filtering and rendering. detail_type
and the category slug are always included for client-side style rules.
Notes¶
A lightweight Note model is planned to attach time-stamped annotations to any
GeoPlace (e.g. "hut burned down", "source dry in summer"). Notes will carry a
severity level and an optional expiry. This is deferred and will be designed
separately.
Out of Scope¶
- Point review workflow and OSM editing integration (Mangrove, MapComplete) — see WEP 009.
Hutmodel migration intoaccommodation.hut— deferred, no timeline.