Map pins at the street address, not the town, when an address has a street #913
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#913
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?
When an address has a street, its pin should sit at that street address, not at the centre of its town or postcode. Moving the pin by hand for one address stays possible and wins over anything worked out.
Decision (2026-10-15)
Street-level geocoding is wanted: this overrides #700's "no online geocoder in core". A point placed by hand or from Wikidata still wins, and an address with no street keeps the offline placement. The research into services (Nominatim, Photon, Pelias, addok/BAN, hosted options) and the recommended design are in the comment of 2026-10-15 below. The "To decide" section at the end is answered there as a recommendation.
What exists today
jobs/placing.py::placeplaces an address when it is saved, offline only. It uses the postcode's centroid wherefetch_geonames --postcodeswas run, and otherwise the town from the GeoNames city table.placed_byrecords which (town|postcode|manual|wikidata). The module's opening says the street "is never read here at all".templates/partials/postal_addresses.html(Your details, a contact's form, a company's form) has a status line ("On the map at …") with Place it by hand / Move it. That link goes tojobs:address_place(jobs/views.py, the view that ends inplacing.place_by_hand). A point placed by hand or from Wikidata (KEPT) is not overwritten when the words change, andtake_off_by_handgives it back to the words. The "change it per entry" half of the request is therefore already built. This issue is about the default point.docs/THREAT-MODEL.mdexplains why. A street address is the most sensitive thing the map touches, and on a homelab the instance's address is the person's.What would change
Street-level placement, in a way #700 allows or by revisiting #700. The options, none decided here:
address_placepage) that names the address and the service before sending one request through the guarded client (postulo.plugins.http). The point is stored as a newplaced_byvalue (street) and kept likemanual. The first plugin could target Nominatim (1 request a second, identifying User-Agent, results cached) or an operator's own Nominatim or Photon.core/gdpr.py) updated.Whatever is chosen:
To decide
Related: #700, #702, #703, #701, #898.
Checked against
mainat56fc9d531.Decision (maintainer, 2026-10-15)
#700's "no online geocoder in core" is overridden: street-level geocoding is wanted. An address with a street gets a pin at that street address, worked out by a geocoding service. The rest of #700/#702 stands: a point placed by hand (
placed_by="manual") or from Wikidata wins and is never overwritten by a geocode; take off gives the point back to the words; an address with no street keeps today's offline town/postcode placement.What follows is the research into which service and how, and a recommended design. Everything below was read on the projects' own pages on 2026-10-15 unless it says otherwise.
The options
nominatim.openstreetmap.org, or self-hosted (PostgreSQL/PostGIS)photon.komoot.ioor self-hosted (Java 21, embedded OpenSearch)-Xmx), updates need double the disk. Has a/structuredendpoint withstreet,housenumber,postcode,city,countrycode(API) — a good fit forPostalAddress's partsapi-adresse.data.gouv.fr, which was to be decommissioned end of January 2026)https://data.geopf.fr/geocodage/search(no key for normal calls), or addok + the BAN addok file self-hostedpypostal)places.pywould be possible but coverage is uneven, the licence must be tracked per source, and house-number matching is a geocoder's job; not recommended for coreWhat the research changes
placed_fromalready gives this)./structuredendpoint maps one-to-one ontoPostalAddress(street, postcode, municipality, region, country) and Nominatim also accepts structuredstreet=/city=/postalcode=/countrycodes=parameters; send parts, not one joined string.Recommendation
A pluggable geocoder in core, small and synchronous, behind the guarded client:
geocode(parts) -> (lat, lon, precision) | None, with backends selected under Server settings → Plugins → Maps (beside the tile source, #898):nominatim(default),photon,addok(BAN/Géoplateforme-compatible),off. Each backend takes a base URL, so an operator points the same backend at their own Nominatim, Photon or addok. Hosted commercial services (Geocode.Earth, OpenCage, LocationIQ) stay out of core; their terms (keys, attribution, storage limits like LocationIQ's 48 h) belong in a plugin if anyone wants one.countrycodesfrom the address's country, the sharedUSER_AGENT(Postulo (+https://source.tiagoagueda.com/postulo/postulo)) plus Nominatim's optionalemailparameter with the operator's address where set (search API),Accept-Languagefrom the person's language, one result.placed_by = "street"(newPlacedchoice, label "its street"),placed_fromas now, so an unchanged save sends nothing. Never overwritemanualorwikidata; a later save that changes the street re-geocodes astreetpoint; take off returns it to the offline placement.docs/THREAT-MODEL.md(the map paragraph) rewritten to say what is now sent and to whom; the processing record (core/gdpr.py) gains the recipient (the configured geocoder) for company addresses, and for personal/contact addresses only under the self-hosted condition above; the data-protection page names the service. Attribution "© OpenStreetMap contributors, ODbL" (or the configured service's) beside the map.New strings ("its street", the setting's label and help, the status line naming the service) go through the catalogues and Weblate as usual.