Maps on OpenStreetMap data: choose the engine, the precision and where the tiles come from #700
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#700
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
CompanyMapView(src/postulo/jobs/views.py:147-199, routecompanies/map/,src/postulo/jobs/urls.py:10) lists the places the person's companies are in, with a count per place, andjobs/company_map.htmldraws 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, inTHIRD-PARTY.md) with one proportional dot per place,viewBox="0 0 360 180", equirectangular. No script, no provider, no request.src/postulo/jobs/places.pyreads GeoNames' cities above a thousand inhabitants (geonames-cities1000.txt, 32 MB, downloaded bymanage.py fetch_geonames, not committed, licence note insrc/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).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.img-src 'self' data:,connect-src 'self'andworker-src 'self'(src/postulo/config/settings/base.py:671-698).django.contrib.gis, GDAL, GEOS, PROJ, SpatiaLite or PostGIS anywhere insrc/ordocker/. The default database is SQLite, PostgreSQL is optional (base.py:143-149); the PostgreSQL compose file runspostgres: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,curlandpostgresql-client-17only (docker/Dockerfile:244-259). Django is 6.1.1 (uv.lock:588).Company.locationis text with nullablelocation_latandlocation_lonfloats (jobs/models.py:273-287).PostalAddresshas 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). ADepartmentis a name only (jobs/models.py:568-601).THIRD-PARTY.md).GEONAMES-LICENCE.mdplaces the world outline insrc/postulo/static/map/; it is insrc/postulo/templates/jobs/map/.Checked against
mainatf09b6010b.Proposal
docs/THREAT-MODEL.md(the paragraph at:48-55is rewritten to whatever is chosen) and in a comment on #108 saying which of its choices are superseded and which stand.kind="feature", likeemployer-structureandphone-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.Companyhas them, and givePostalAddressthe same columns in #702; no spatial column type.Decisions
employer-structuredocuments (“only the declaration lives here”).src/postulo/core/gdpr.py) must then list coordinates for contacts.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).places.pyalready 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.django-cities-xtdexists and is maintained (below) but needs a spatial database; it would import GeoNames into per-instance tables to do whatplaces.pyalready 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).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.arthanson/django-cities-xtd, a fork ofdjango-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-light3.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.Point, fixed in 2.5.0 itself) wraps geocoders; its HTTP goes through its own adapters rather thanpostulo.plugins.http(not verified for 2.5.0), and a geocode here is one GET.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
docs/THREAT-MODEL.md:48-55says what the map now does and does not send; #108 carries a comment saying what is superseded.mapsfeature appears under Server settings → Plugins, off hides the map page and stops place resolution without deleting a coordinate, on is today's behaviour. Tests intests/test_company_map.py(switch off and on, stored coordinates kept) andtests/test_plugin_surface.py;tests/test_wiki_surface.pyonly ifpostulo.plugins.apigains a name.THIRD-PARTY.mdcarries GeoNames, and any library adopted (#579);tests/test_trademarks.pyholds the table to the tree; the licence-note path is corrected.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.
Decisions, as accepted:
mapsfeature plugin, with the data in core.Implemented in this issue: the
mapsswitch, the GeoNames row inTHIRD-PARTY.md, the corrected licence-note path, and the rewrittendocs/THREAT-MODEL.mdparagraph.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.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.