Maps on OpenStreetMap data: choose the engine, the precision and where the tiles come from #700

Closed
opened 2026-10-03 01:59:10 +00:00 by tiagoagueda · 3 comments
Owner

From the list: the map is “still needs work and is not ready. Should be built upon GeoDjango, or other alternative, preferably with OpenStreetMap data”, and pins are wanted for companies with a Wikidata location, companies with a full address, contacts (person, department, user) with a full address, and several addresses as several pins. That reverses parts of #108 (closed), which chose city precision, no geocoder and no tiles on purpose. This is the decision issue and the tracker for #701, #702 and #703; it settles the questions they share before any of them is built.

What exists today

  • The map is core, not a plugin. CompanyMapView (src/postulo/jobs/views.py:147-199, route companies/map/, src/postulo/jobs/urls.py:10) lists the places the person's companies are in, with a count per place, and jobs/company_map.html draws the same list as an SVG the server writes: a Natural Earth 1:110m outline (src/postulo/templates/jobs/map/world_outline.html, public domain, in THIRD-PARTY.md) with one proportional dot per place, viewBox="0 0 360 180", equirectangular. No script, no provider, no request.
  • Places are resolved offline. src/postulo/jobs/places.py reads GeoNames' cities above a thousand inhabitants (geonames-cities1000.txt, 32 MB, downloaded by manage.py fetch_geonames, not committed, licence note in src/postulo/jobs/data/GEONAMES-LICENCE.md), matches “City, Country” against it when a company's location is saved, keeps what text it came from, when, and whether a person corrected it (jobs/models.py:273-287, Company.apply_location_guess). The module's own opening says why: a geocoding service “would be sent the name of a company this person is applying to” (places.py:1-22).
  • The line is written down. docs/THREAT-MODEL.md:48-55: “The map knows the city and stops there … a search at street precision is a map of where the person will be at nine in the morning … Nothing about a location is ever sent to a geocoding service to be placed, and the map is SVG the server itself writes, so a tile server never sees where somebody is applying.” #108 weighed three ways to a basemap (none; Postulo proxies tiles; self-hosted tiles) and recommended none.
  • The security policy would refuse a tile layer as it stands. img-src 'self' data:, connect-src 'self' and worker-src 'self' (src/postulo/config/settings/base.py:671-698).
  • No spatial stack exists. No django.contrib.gis, GDAL, GEOS, PROJ, SpatiaLite or PostGIS anywhere in src/ or docker/. The default database is SQLite, PostgreSQL is optional (base.py:143-149); the PostgreSQL compose file runs postgres:17-alpine (docker/compose.postgres.yml:86), which is not a PostGIS image; the runtime image's apt layer installs Pango, HarfBuzz, fontconfig, fonts, ca-certificates, curl and postgresql-client-17 only (docker/Dockerfile:244-259). Django is 6.1.1 (uv.lock:588).
  • What holds a place. Company.location is text with nullable location_lat and location_lon floats (jobs/models.py:273-287). PostalAddress has street, postcode, town, region and country and no coordinates (src/postulo/core/models.py:486-570); it belongs to a profile or a contact, and not to a company, which has none yet (#329, open). A Department is a name only (jobs/models.py:568-601).
  • Open map defects: #534 (the country hint reads GeoNames' capital and postal-code columns as country names), #399 (the city index is built inside the first save of each worker, 6 s and 190 MB), #366 (the container downloads GeoNames unasked), #579 (the GeoNames and NACE data are missing from THIRD-PARTY.md).
  • A small drift to fix on the way: GEONAMES-LICENCE.md places the world outline in src/postulo/static/map/; it is in src/postulo/templates/jobs/map/.

Checked against main at f09b6010b.

Proposal

  1. Record the decisions below in the issue, in docs/THREAT-MODEL.md (the paragraph at :48-55 is rewritten to whatever is chosen) and in a comment on #108 saying which of its choices are superseded and which stand.
  2. Make the map a shipped feature plugin (kind="feature", like employer-structure and phone-numbers): one switch decides whether the map, the place resolution and any pin are offered; switched off, nothing is deleted and nothing is shown. The coordinates and the models stay in core, because a plugin cannot own a table.
  3. Keep coordinates as plain floats with a source and a date, as Company has them, and give PostalAddress the same columns in #702; no spatial column type.
  4. Add the new data sources in order: Wikidata's coordinate (#701, 1.0.0, after #673), then addresses (#702), then several per holder (#703).
  5. Housekeeping inside this issue: the licence-note path above, and a THIRD-PARTY row for any new vendored library (#579 is the register's open gap).

Decisions

  • GeoDjango or plain coordinates. GeoDjango needs the GEOS, GDAL and PROJ libraries and a spatial database: PostGIS on PostgreSQL, or SpatiaLite on SQLite 3.37.0 or later (Django's installation page, read 2026-10-02). It would change the default database engine, the image, the PostgreSQL image, CI and every contributor's setup. What the map needs is a point per place and a count; “near me” over a few hundred rows is a great-circle distance in Python. Recommended: no GeoDjango. Reconsider only if a feature appears that needs a spatial query over thousands of rows.
  • Plugin or core. The list calls it a built-in plugin. Recommended: a shipped feature plugin with the data in core, the shape employer-structure documents (“only the declaration lives here”).
  • Where the line on precision sits. City level only (today); city plus postcode centroid; street level. A pin at street level is meaningless on a world outline: it needs a zoomable basemap, which is the tile question below. Recommended: city level stays the default and the overview stays the SVG; a street-level pin exists only as an explicit act on one address (#702), stored coordinates never leave the instance, a person's own home address is never pinned unless they ask, and a contact's address (somebody else's personal data) is pinned only on request. This supersedes #108's “street precision is not a column to keep” for addresses a person deliberately places; it keeps it for everything else. The processing record (src/postulo/core/gdpr.py) must then list coordinates for contacts.
  • Where a basemap's tiles come from. (a) None: the SVG as today; (b) the browser fetches tiles from OpenStreetMap's public server; (c) the instance proxies and caches the public tiles; (d) the operator provides tiles on the instance's own origin (a PMTiles extract or any tile URL on that origin). (b) is blocked by the CSP, would tell a third party the viewport and the person's address, and the tile policy limits it (below). (c) keeps the CSP (img-src 'self') and hides the browser, but sends the instance's address and every tile coordinate to OpenStreetMap, and on a homelab the instance's address is the person's; it hides who, not what. (d) is the only option that sends nothing anywhere; the planet in PMTiles is about 120 GB at zoom 0 to 15 and a region can be extracted, with each extra zoom level roughly doubling the size. Recommended: (a) stays the default; a zoomable per-company map exists only when the operator has set a tile source, (d) first, (c) a documented opt-in with the warning above. Not verified: whether the tile policy accepts a caching proxy run by each instance; read the policy again before building (c).
  • Which map library. Recommended: Leaflet, vendored, with same-origin raster tiles so the CSP does not change. MapLibre GL JS needs WebGL and Web Workers (a CSP change is likely; not verified for 6.x) and has had a critical advisory (below).
  • Geocoding a full address. Offline first: GeoNames' postal-code export gives a centroid per postcode for nearly 100 countries (accuracy field 1 to 6; some countries partial) and is the same kind of file places.py already reads. Online second, only as a separate optional plugin kind, per address, per press, through the guarded client, with the address named before it is sent (the request carries a street address, the most sensitive thing this issue touches). Recommended: postcode centroids in #702; no online geocoder in core; an optional geocoder plugin later if the maintainer wants it.
  • The maintainer's examples. django-cities-xtd exists and is maintained (below) but needs a spatial database; it would import GeoNames into per-instance tables to do what places.py already does from the file. Recommended: do not adopt, and revisit only together with GeoDjango.

Prior art

Verified on 2026-10-02 from the projects' pages, PyPI and npm JSON, and OSV (api.osv.dev).

  • GeoDjango (part of Django, BSD-3-Clause): requirements as above. Reject (decision above).
  • Leaflet 1.9.4, BSD-2-Clause, released 2023-05-18, no dependencies, about 42 KB of JS, no advisories on OSV (npm). A 2.0.0 alpha exists; the 1.x line has had no stable release since May 2023. Adopt, vendored with a THIRD-PARTY row and notice.
  • MapLibre GL JS 6.11.2, BSD-3-Clause, released 2026-09-24, many dependencies. OSV lists GHSA-jrc7-96c5-q579, an XSS through the attribution control's sanitizer, rated critical, fixed in 6.4.1, so the current release is not affected. Reject for now: WebGL, workers and a bigger surface for a page that draws pins.
  • PMTiles 4.5.0 (BSD-3-Clause, 2026-08-10) and protomaps-leaflet 5.1.0 (BSD-3-Clause, 2025-06-18), no advisories on OSV: the reader for option (d). Basemap licence is ODbL as a Produced Work with OpenStreetMap attribution.
  • OpenStreetMap tile policy (tile.openstreetmap.org): attribution “© OpenStreetMap contributors”; a valid User-Agent that identifies the application; honour cache headers or cache each tile at least 7 days; no offline use and no pre-emptive fetching; they may block access without notice if usage degrades the service; best effort, no SLA; anonymised aggregate usage is published. ODbL: credit OpenStreetMap and say the data is under the ODbL; adaptations of the data are shared under the same licence. Whether a private cache of a few geocoded points is a derivative database is not verified.
  • Nominatim (public instance): at most 1 request a second, an identifying User-Agent or Referer, results must be cached, no client-side autocomplete, bulk geocoding not encouraged; small extractions “likely to be covered by fair usage”. Self-hosting a planet needs at least 1 TB of disk and 128 GB of RAM and about 2.5 days (its installation page); a country extract's needs are not stated there.
  • Photon (Apache-2.0): geocoder on OpenSearch and OSM data; Java 21; a planet database of about 95 GB and 64 GB of RAM recommended; its public demo is fair use with no guarantees. Reject for core.
  • django-cities-xtd 0.7.1, MIT, released 2026-01-08, GitHub arthanson/django-cities-xtd, a fork of django-cities; its requirements page says the database must support spatial queries and points at GeoDjango; its README lists Django up to 6.0 (Postulo runs 6.1.1; compatibility not verified); no advisories. django-cities-light 3.11 (MIT, 2026-02-16) needs no GIS and imports cities of 15,000 inhabitants and over by default. Neither is adopted: both duplicate a GeoNames file read.
  • geopy 2.5.0 (MIT, 2026-07-12; OSV lists one moderate advisory, a ReDoS in geopy.Point, fixed in 2.5.0 itself) wraps geocoders; its HTTP goes through its own adapters rather than postulo.plugins.http (not verified for 2.5.0), and a geocode here is one GET.
  • Where Postulo writes a standard by hand (applications/ical.py, documents/docx.py): the precedent does not apply; here a maintained library exists for the part that is adopted (Leaflet) and the part that is not (GeoDjango) is a different engine, not a rewrite.

Builds on

Issues #108 (the map and what it chose; superseded in part), #329 (companies get postal addresses), #366 (the outbound inventory), #534, #399, #579.

Needed by

#701, #702, #703.

Done when

  • The maintainer's answers to the decisions above are on this issue; docs/THREAT-MODEL.md:48-55 says what the map now does and does not send; #108 carries a comment saying what is superseded.
  • If the plugin switch is taken: a shipped maps feature appears under Server settings → Plugins, off hides the map page and stops place resolution without deleting a coordinate, on is today's behaviour. Tests in tests/test_company_map.py (switch off and on, stored coordinates kept) and tests/test_plugin_surface.py; tests/test_wiki_surface.py only if postulo.plugins.api gains a name.
  • THIRD-PARTY.md carries GeoNames, and any library adopted (#579); tests/test_trademarks.py holds the table to the tree; the licence-note path is corrected.
  • No record type is added here, so the archive format (29 at f09b6010b) and the candidate format (4) stay as they are; the standing checklist for a new record type (archive bump, API, search, CV print choice, processing record, page coverage, isolation sweep, catalogues, Weblate #349) belongs to #702.

From the list of improvements of 2026-10-02.

From the list: the map is “still needs work and is not ready. Should be built upon GeoDjango, or other alternative, preferably with OpenStreetMap data”, and pins are wanted for companies with a Wikidata location, companies with a full address, contacts (person, department, user) with a full address, and several addresses as several pins. That reverses parts of #108 (closed), which chose city precision, no geocoder and no tiles on purpose. This is the decision issue and the tracker for #701, #702 and #703; it settles the questions they share before any of them is built. ## What exists today - **The map is core, not a plugin.** `CompanyMapView` (`src/postulo/jobs/views.py:147-199`, route `companies/map/`, `src/postulo/jobs/urls.py:10`) lists the places the person's companies are in, with a count per place, and `jobs/company_map.html` draws the same list as an SVG the server writes: a Natural Earth 1:110m outline (`src/postulo/templates/jobs/map/world_outline.html`, public domain, in `THIRD-PARTY.md`) with one proportional dot per place, `viewBox="0 0 360 180"`, equirectangular. No script, no provider, no request. - **Places are resolved offline.** `src/postulo/jobs/places.py` reads GeoNames' cities above a thousand inhabitants (`geonames-cities1000.txt`, 32 MB, downloaded by `manage.py fetch_geonames`, not committed, licence note in `src/postulo/jobs/data/GEONAMES-LICENCE.md`), matches “City, Country” against it when a company's location is saved, keeps what text it came from, when, and whether a person corrected it (`jobs/models.py:273-287`, `Company.apply_location_guess`). The module's own opening says why: a geocoding service “would be sent the name of a company this person is applying to” (`places.py:1-22`). - **The line is written down.** `docs/THREAT-MODEL.md:48-55`: “The map knows the city and stops there … a search at street precision is a map of where the person will be at nine in the morning … Nothing about a location is ever sent to a geocoding service to be placed, and the map is SVG the server itself writes, so a tile server never sees where somebody is applying.” #108 weighed three ways to a basemap (none; Postulo proxies tiles; self-hosted tiles) and recommended none. - **The security policy would refuse a tile layer as it stands.** `img-src 'self' data:`, `connect-src 'self'` and `worker-src 'self'` (`src/postulo/config/settings/base.py:671-698`). - **No spatial stack exists.** No `django.contrib.gis`, GDAL, GEOS, PROJ, SpatiaLite or PostGIS anywhere in `src/` or `docker/`. The default database is SQLite, PostgreSQL is optional (`base.py:143-149`); the PostgreSQL compose file runs `postgres:17-alpine` (`docker/compose.postgres.yml:86`), which is not a PostGIS image; the runtime image's apt layer installs Pango, HarfBuzz, fontconfig, fonts, `ca-certificates`, `curl` and `postgresql-client-17` only (`docker/Dockerfile:244-259`). Django is 6.1.1 (`uv.lock:588`). - **What holds a place.** `Company.location` is text with nullable `location_lat` and `location_lon` floats (`jobs/models.py:273-287`). `PostalAddress` has street, postcode, town, region and country and no coordinates (`src/postulo/core/models.py:486-570`); it belongs to a profile or a contact, and **not** to a company, which has none yet (#329, open). A `Department` is a name only (`jobs/models.py:568-601`). - **Open map defects:** #534 (the country hint reads GeoNames' capital and postal-code columns as country names), #399 (the city index is built inside the first save of each worker, 6 s and 190 MB), #366 (the container downloads GeoNames unasked), #579 (the GeoNames and NACE data are missing from `THIRD-PARTY.md`). - A small drift to fix on the way: `GEONAMES-LICENCE.md` places the world outline in `src/postulo/static/map/`; it is in `src/postulo/templates/jobs/map/`. Checked against `main` at `f09b6010b`. ## Proposal 1. **Record the decisions below** in the issue, in `docs/THREAT-MODEL.md` (the paragraph at `:48-55` is rewritten to whatever is chosen) and in a comment on #108 saying which of its choices are superseded and which stand. 2. **Make the map a shipped feature plugin** (`kind="feature"`, like `employer-structure` and `phone-numbers`): one switch decides whether the map, the place resolution and any pin are offered; switched off, nothing is deleted and nothing is shown. The coordinates and the models stay in core, because a plugin cannot own a table. 3. **Keep coordinates as plain floats with a source and a date**, as `Company` has them, and give `PostalAddress` the same columns in #702; no spatial column type. 4. **Add the new data sources in order**: Wikidata's coordinate (#701, 1.0.0, after #673), then addresses (#702), then several per holder (#703). 5. **Housekeeping inside this issue**: the licence-note path above, and a THIRD-PARTY row for any new vendored library (#579 is the register's open gap). ## Decisions - **GeoDjango or plain coordinates.** GeoDjango needs the GEOS, GDAL and PROJ libraries and a spatial database: PostGIS on PostgreSQL, or SpatiaLite on SQLite 3.37.0 or later (Django's installation page, read 2026-10-02). It would change the default database engine, the image, the PostgreSQL image, CI and every contributor's setup. What the map needs is a point per place and a count; “near me” over a few hundred rows is a great-circle distance in Python. **Recommended: no GeoDjango.** Reconsider only if a feature appears that needs a spatial query over thousands of rows. - **Plugin or core.** The list calls it a built-in plugin. **Recommended: a shipped feature plugin with the data in core**, the shape `employer-structure` documents (“only the declaration lives here”). - **Where the line on precision sits.** City level only (today); city plus postcode centroid; street level. A pin at street level is meaningless on a world outline: it needs a zoomable basemap, which is the tile question below. **Recommended: city level stays the default and the overview stays the SVG; a street-level pin exists only as an explicit act on one address** (#702), stored coordinates never leave the instance, a person's own home address is never pinned unless they ask, and a contact's address (somebody else's personal data) is pinned only on request. This supersedes #108's “street precision is not a column to keep” for addresses a person deliberately places; it keeps it for everything else. The processing record (`src/postulo/core/gdpr.py`) must then list coordinates for contacts. - **Where a basemap's tiles come from.** (a) None: the SVG as today; (b) the browser fetches tiles from OpenStreetMap's public server; (c) the instance proxies and caches the public tiles; (d) the operator provides tiles on the instance's own origin (a PMTiles extract or any tile URL on that origin). (b) is blocked by the CSP, would tell a third party the viewport and the person's address, and the tile policy limits it (below). (c) keeps the CSP (`img-src 'self'`) and hides the browser, **but sends the instance's address and every tile coordinate to OpenStreetMap, and on a homelab the instance's address is the person's**; it hides who, not what. (d) is the only option that sends nothing anywhere; the planet in PMTiles is about 120 GB at zoom 0 to 15 and a region can be extracted, with each extra zoom level roughly doubling the size. **Recommended: (a) stays the default; a zoomable per-company map exists only when the operator has set a tile source, (d) first, (c) a documented opt-in with the warning above.** Not verified: whether the tile policy accepts a caching proxy run by each instance; read the policy again before building (c). - **Which map library.** **Recommended: Leaflet**, vendored, with same-origin raster tiles so the CSP does not change. MapLibre GL JS needs WebGL and Web Workers (a CSP change is likely; not verified for 6.x) and has had a critical advisory (below). - **Geocoding a full address.** Offline first: GeoNames' postal-code export gives a centroid per postcode for nearly 100 countries (accuracy field 1 to 6; some countries partial) and is the same kind of file `places.py` already reads. Online second, only as a separate optional plugin kind, per address, per press, through the guarded client, with the address named before it is sent (the request carries a street address, the most sensitive thing this issue touches). **Recommended: postcode centroids in #702; no online geocoder in core; an optional geocoder plugin later if the maintainer wants it.** - **The maintainer's examples.** `django-cities-xtd` exists and is maintained (below) but needs a spatial database; it would import GeoNames into per-instance tables to do what `places.py` already does from the file. **Recommended: do not adopt**, and revisit only together with GeoDjango. ## Prior art Verified on 2026-10-02 from the projects' pages, PyPI and npm JSON, and OSV (`api.osv.dev`). - **GeoDjango** (part of Django, BSD-3-Clause): requirements as above. Reject (decision above). - **Leaflet 1.9.4**, BSD-2-Clause, released 2023-05-18, no dependencies, about 42 KB of JS, no advisories on OSV (npm). A 2.0.0 alpha exists; the 1.x line has had no stable release since May 2023. **Adopt**, vendored with a THIRD-PARTY row and notice. - **MapLibre GL JS 6.11.2**, BSD-3-Clause, released 2026-09-24, many dependencies. OSV lists GHSA-jrc7-96c5-q579, an XSS through the attribution control's sanitizer, rated critical, fixed in 6.4.1, so the current release is not affected. **Reject for now**: WebGL, workers and a bigger surface for a page that draws pins. - **PMTiles 4.5.0** (BSD-3-Clause, 2026-08-10) and **protomaps-leaflet 5.1.0** (BSD-3-Clause, 2025-06-18), no advisories on OSV: the reader for option (d). Basemap licence is ODbL as a Produced Work with OpenStreetMap attribution. - **OpenStreetMap tile policy** (`tile.openstreetmap.org`): attribution “© OpenStreetMap contributors”; a valid User-Agent that identifies the application; honour cache headers or cache each tile at least 7 days; no offline use and no pre-emptive fetching; they may block access without notice if usage degrades the service; best effort, no SLA; anonymised aggregate usage is published. **ODbL:** credit OpenStreetMap and say the data is under the ODbL; adaptations of the data are shared under the same licence. Whether a private cache of a few geocoded points is a derivative database is not verified. - **Nominatim** (public instance): at most 1 request a second, an identifying User-Agent or Referer, results must be cached, no client-side autocomplete, bulk geocoding not encouraged; small extractions “likely to be covered by fair usage”. Self-hosting a planet needs at least 1 TB of disk and 128 GB of RAM and about 2.5 days (its installation page); a country extract's needs are not stated there. - **Photon** (Apache-2.0): geocoder on OpenSearch and OSM data; Java 21; a planet database of about 95 GB and 64 GB of RAM recommended; its public demo is fair use with no guarantees. Reject for core. - **django-cities-xtd 0.7.1**, MIT, released 2026-01-08, GitHub `arthanson/django-cities-xtd`, a fork of `django-cities`; its requirements page says the database must support spatial queries and points at GeoDjango; its README lists Django up to 6.0 (Postulo runs 6.1.1; compatibility not verified); no advisories. `django-cities-light` 3.11 (MIT, 2026-02-16) needs no GIS and imports cities of 15,000 inhabitants and over by default. Neither is adopted: both duplicate a GeoNames file read. - **geopy 2.5.0** (MIT, 2026-07-12; OSV lists one moderate advisory, a ReDoS in `geopy.Point`, fixed in 2.5.0 itself) wraps geocoders; its HTTP goes through its own adapters rather than `postulo.plugins.http` (not verified for 2.5.0), and a geocode here is one GET. - **Where Postulo writes a standard by hand** (`applications/ical.py`, `documents/docx.py`): the precedent does not apply; here a maintained library exists for the part that is adopted (Leaflet) and the part that is not (GeoDjango) is a different engine, not a rewrite. ## Builds on Issues #108 (the map and what it chose; superseded in part), #329 (companies get postal addresses), #366 (the outbound inventory), #534, #399, #579. ## Needed by #701, #702, #703. ## Done when - The maintainer's answers to the decisions above are on this issue; `docs/THREAT-MODEL.md:48-55` says what the map now does and does not send; #108 carries a comment saying what is superseded. - If the plugin switch is taken: a shipped `maps` feature appears under Server settings → Plugins, off hides the map page and stops place resolution without deleting a coordinate, on is today's behaviour. Tests in `tests/test_company_map.py` (switch off and on, stored coordinates kept) and `tests/test_plugin_surface.py`; `tests/test_wiki_surface.py` only if `postulo.plugins.api` gains a name. - `THIRD-PARTY.md` carries GeoNames, and any library adopted (#579); `tests/test_trademarks.py` holds the table to the tree; the licence-note path is corrected. - No record type is added here, so the archive format (29 at `f09b6010b`) and the candidate format (4) stay as they are; the standing checklist for a new record type (archive bump, API, search, CV print choice, processing record, page coverage, isolation sweep, catalogues, Weblate #349) belongs to #702. From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.6.0 milestone 2026-10-03 01:59:10 +00:00
Author
Owner

Decisions, as accepted:

  1. No GeoDjango. Coordinates stay plain floats with a source and a date.
  2. The map is a shipped maps feature plugin, with the data in core.
  3. City level stays the default and the overview stays the server-drawn SVG. A street-level pin exists only as an explicit act on one address (#702). A person's own home is never pinned unless they ask, and a contact's address only on request.
  4. Tiles: none by default. A zoomable map exists only when the operator sets a tile source. First choice is tiles on the instance's own origin. A caching proxy of OpenStreetMap is a documented opt-in with the warning that it hides who, not what.
  5. Leaflet, vendored, with same-origin raster tiles (in #702).
  6. Postcode centroids offline in #702, no online geocoder in core, and django-cities-xtd is not adopted.

Implemented in this issue: the maps switch, the GeoNames row in THIRD-PARTY.md, the corrected licence-note path, and the rewritten docs/THREAT-MODEL.md paragraph.

Decisions, as accepted: 1. No GeoDjango. Coordinates stay plain floats with a source and a date. 2. The map is a shipped `maps` feature plugin, with the data in core. 3. City level stays the default and the overview stays the server-drawn SVG. A street-level pin exists only as an explicit act on one address (#702). A person's own home is never pinned unless they ask, and a contact's address only on request. 4. Tiles: none by default. A zoomable map exists only when the operator sets a tile source. First choice is tiles on the instance's own origin. A caching proxy of OpenStreetMap is a documented opt-in with the warning that it hides who, not what. 5. Leaflet, vendored, with same-origin raster tiles (in #702). 6. Postcode centroids offline in #702, no online geocoder in core, and django-cities-xtd is not adopted. Implemented in this issue: the `maps` switch, the GeoNames row in `THIRD-PARTY.md`, the corrected licence-note path, and the rewritten `docs/THREAT-MODEL.md` paragraph.
Author
Owner

Done in 3b45a41d7: the six decisions are recorded above, the map is the shipped maps feature (off hides it and keeps every coordinate), docs/THREAT-MODEL.md is rewritten, and the licence-note path is fixed. The wiki's Configuration page is updated. The pins, addresses and tiles continue in #701, #702 and #703.

Done in 3b45a41d7: the six decisions are recorded above, the map is the shipped maps feature (off hides it and keeps every coordinate), docs/THREAT-MODEL.md is rewritten, and the licence-note path is fixed. The wiki's Configuration page is updated. The pins, addresses and tiles continue in #701, #702 and #703.
Author
Owner

Decision 4 is reversed by the maintainer on 2026-10-12: OpenStreetMap's public tiles are the default, fetched by the browser, with the tile source (OpenStreetMap, own tiles, none) under Server settings. Delivered in #898. Decisions 1, 2, 3, 5 and 6 stand.

Decision 4 is reversed by the maintainer on 2026-10-12: OpenStreetMap's public tiles are the default, fetched by the browser, with the tile source (OpenStreetMap, own tiles, none) under Server settings. Delivered in #898. Decisions 1, 2, 3, 5 and 6 stand.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#700
No description provided.