Place a postal address on the map: companies, contacts and the person's own, each only when asked #702

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

From the list: “Companies with full address = coordinates = pin on a map. Contact (Person, Department, User) with full address = coordinates = pin on a map.” A postal address has no coordinates, a company has no postal address yet, and the map's rule today is city level and nothing finer. The engine, the precision and the privacy line are decided in #700; this issue puts coordinates on addresses and decides who is pinned by default.

What exists today

  • An address is parts and no place. PostalAddress holds kind, label, street, postcode, town, region, country and a primary flag, through a generic relation, and no coordinates (src/postulo/core/models.py:486-570). It is unique per owner and deliberately not across the instance, so two accounts can hold one address (#92). “Valid cannot mean verified”: nothing about an address is looked up (docs/THREAT-MODEL.md:30-47).
  • Address rules per country exist (#306). RULES in src/postulo/plugins/postal_rules/rules.py:135 holds 37 countries' postcode forms and what an address carries; the plugin says “a format check is not a verification”: nothing there knows whether an address exists or whether a town goes with a postcode. A postcode's form is checked; its place is not known anywhere.
  • Who can hold one. A profile (the person's own, src/postulo/accounts/models.py:282) and a contact (src/postulo/jobs/models.py:635). A company cannot (#329, open, unmilestoned), and a Department is a name only (jobs/models.py:568-601; #329 recommends no addresses for departments for now). “Department” in the list is therefore not possible to pin until one of those changes.
  • The map knows a company's town. Company.location resolves to a city coordinate offline (src/postulo/jobs/places.py, Company.apply_location_guess); an address's town and country could be resolved the same way with no new data.
  • Data about other people is handled separately. A contact's addresses are in the contact export (src/postulo/core/gdpr.py:101-114, contact_document) and erased with the contact (erase_contact, :277); the record of processing is drawn from the installed plugins (:379). The threat model says a leaked home address is “somewhere somebody can be found”, and a CV prints a town and country and never a street.
  • The map's page is a list first: every place is text before it is drawn (src/postulo/jobs/views.py:147-199, src/postulo/templates/jobs/company_map.html).
  • Postal-code data exists offline (verified 2026-10-02): GeoNames' postal-code export gives a centroid per postcode for nearly 100 countries, with an accuracy field from 1 (estimated) to 6 (centroid of addresses or shape), under Creative Commons Attribution 4.0, about 20 MB zipped (allCountries.zip, 19.7 MB); some countries are partial (for Canada, the Netherlands and the United Kingdom allCountries.zip holds only the first part of each code), and its readme gives the UK file a Royal Mail copyright notice of its own. It is a file of the same kind places.py already reads.

Checked against main at f09b6010b.

Proposal

  1. Coordinates on an address: latitude, longitude, where they came from (town, postcode, a street-level place the person asked for, set by hand) and when, on PostalAddress; nullable, never required, never shown as a field to type.
  2. Offline resolution on save, to town level (the address's town and country against the city table, no new data) and, where the country's postal-code file is present, to postcode level. Nothing leaves the instance; a failed match is not an error, it is an address the map does not draw.
  3. Street level only by an act on one address, as decided in #700 (an optional geocoder, per press, naming the address before it is sent). Not part of this issue's offline path.
  4. Who is pinned by default. Companies (once #329 gives them addresses): their addresses are pinned at town level. Contacts: not pinned until the person asks. The person's own addresses: never on the overview, and a pin only when they ask. Departments: not until they can hold an address.
  5. The map page lists what is pinned, each pin as text (whose address, which kind, town and country; the street only on the detail view, never on the overview), with the same one-dot-per-place overview and the precise pins only where #700 provides a basemap.
  6. Third parties' data stays handled as such: a contact's coordinates are in the contact export, erased with the contact and named in what the map plugin says it processes.
  7. The archive and the importer carry the new columns with the address rows.

Decisions

  • Defaults per holder (above). The list asks for pins for all of them. Recommended: company on, contact on request, the person's own on request, because a contact's and the person's own addresses are somebody's home, and the map sits beside the CV in one database; this is the threat model's own reasoning (:48-55) carried to addresses.
  • Columns on the address or a separate table of pins. Recommended: columns on PostalAddress: one address has one point; a pin table would need its own ownership, deletion and archive handling for nothing.
  • Resolve on save or on press. Recommended: offline resolution on save (as a company's location is), network never; a street-level geocode is a press.
  • Postcode data. Adopt GeoNames' postal-code export as a second offline file, next to the city table, with its notice and a THIRD-PARTY row (#579), or stay at town level. Recommended: add it, optional: it is the step between a city and a street that costs no request. Not verified: how well it covers the countries the postal-rules plugin handles; and the UK part carries a Royal Mail notice that has to be read before it is shipped (leave a country out rather than ship it unread).
  • Department. Recommended: leave out here, and say so; it follows #329's decision on departments.

Builds on

Issues #92 (several addresses, not unique across the instance), #306 (an address checked against its country), #329 (companies get addresses), #366, #579. #700 decides engine and precision; #701 is the precise point from Wikidata.

Needed by

#703 (several addresses, several pins).

Done when

  • tests/test_postal_addresses.py: an address with a known town and country is resolved to a town-level point on save and a postcode-level point where the file has it; an unknown one stays unplaced without an error; a second owner never sees the first's pins; a contact's address is not pinned until asked.
  • tests/test_company_map.py: pins appear in the list as text and, where drawn, match it; an unpinned contact never appears; the person's own address never appears on the overview.
  • tests/test_gdpr.py: a contact's export carries its coordinates and erasing the contact removes them. tests/test_export.py: the archive format goes up by one (src/postulo/core/export.py FORMAT_VERSION: one more than whatever FORMAT_VERSION is when the commit lands (29 at f09b6010b); tests/test_export.py:96 follows it) and the round trip keeps them; the candidate format (4) does not change, because addresses are not in the career block.
  • Standing checklist for a new data point: API (ProfileOut and the contact and company schemas: expose or deliberately omit, said in the schema), search (coordinates are not searched), CV print choice (#308: a pin never prints), the processing record (src/postulo/core/gdpr.py), tests/test_wiki_surface.py for any new plugin-surface name, tests/test_page_coverage.py and tests/security/test_isolation_sweep.py for any new page, strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349).

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

From the list: “Companies with full address = coordinates = pin on a map. Contact (Person, Department, User) with full address = coordinates = pin on a map.” A postal address has no coordinates, a company has no postal address yet, and the map's rule today is city level and nothing finer. The engine, the precision and the privacy line are decided in #700; this issue puts coordinates on addresses and decides who is pinned by default. ## What exists today - **An address is parts and no place.** `PostalAddress` holds kind, label, street, postcode, town, region, country and a primary flag, through a generic relation, and no coordinates (`src/postulo/core/models.py:486-570`). It is unique per owner and deliberately not across the instance, so two accounts can hold one address (#92). “Valid cannot mean verified”: nothing about an address is looked up (`docs/THREAT-MODEL.md:30-47`). - **Address rules per country exist (#306).** `RULES` in `src/postulo/plugins/postal_rules/rules.py:135` holds 37 countries' postcode forms and what an address carries; the plugin says “a format check is not a verification”: nothing there knows whether an address exists or whether a town goes with a postcode. A postcode's *form* is checked; its *place* is not known anywhere. - **Who can hold one.** A profile (the person's own, `src/postulo/accounts/models.py:282`) and a contact (`src/postulo/jobs/models.py:635`). **A company cannot** (#329, open, unmilestoned), and a `Department` is a name only (`jobs/models.py:568-601`; #329 recommends no addresses for departments for now). “Department” in the list is therefore not possible to pin until one of those changes. - **The map knows a company's town.** `Company.location` resolves to a city coordinate offline (`src/postulo/jobs/places.py`, `Company.apply_location_guess`); an address's town and country could be resolved the same way with no new data. - **Data about other people is handled separately.** A contact's addresses are in the contact export (`src/postulo/core/gdpr.py:101-114`, `contact_document`) and erased with the contact (`erase_contact`, `:277`); the record of processing is drawn from the installed plugins (`:379`). The threat model says a leaked home address is “somewhere somebody can be found”, and a CV prints a town and country and never a street. - **The map's page is a list first**: every place is text before it is drawn (`src/postulo/jobs/views.py:147-199`, `src/postulo/templates/jobs/company_map.html`). - **Postal-code data exists offline** (verified 2026-10-02): GeoNames' postal-code export gives a centroid per postcode for nearly 100 countries, with an accuracy field from 1 (estimated) to 6 (centroid of addresses or shape), under Creative Commons Attribution 4.0, about 20 MB zipped (`allCountries.zip`, 19.7 MB); some countries are partial (for Canada, the Netherlands and the United Kingdom `allCountries.zip` holds only the first part of each code), and its readme gives the UK file a Royal Mail copyright notice of its own. It is a file of the same kind `places.py` already reads. Checked against `main` at `f09b6010b`. ## Proposal 1. **Coordinates on an address**: `latitude`, `longitude`, where they came from (town, postcode, a street-level place the person asked for, set by hand) and when, on `PostalAddress`; nullable, never required, never shown as a field to type. 2. **Offline resolution on save, to town level** (the address's town and country against the city table, no new data) and, where the country's postal-code file is present, to postcode level. Nothing leaves the instance; a failed match is not an error, it is an address the map does not draw. 3. **Street level only by an act on one address**, as decided in #700 (an optional geocoder, per press, naming the address before it is sent). Not part of this issue's offline path. 4. **Who is pinned by default.** Companies (once #329 gives them addresses): their addresses are pinned at town level. Contacts: not pinned until the person asks. The person's own addresses: never on the overview, and a pin only when they ask. Departments: not until they can hold an address. 5. **The map page lists what is pinned**, each pin as text (whose address, which kind, town and country; the street only on the detail view, never on the overview), with the same one-dot-per-place overview and the precise pins only where #700 provides a basemap. 6. **Third parties' data stays handled as such**: a contact's coordinates are in the contact export, erased with the contact and named in what the map plugin says it processes. 7. **The archive and the importer carry the new columns** with the address rows. ## Decisions - **Defaults per holder** (above). The list asks for pins for all of them. **Recommended: company on, contact on request, the person's own on request**, because a contact's and the person's own addresses are somebody's home, and the map sits beside the CV in one database; this is the threat model's own reasoning (`:48-55`) carried to addresses. - **Columns on the address or a separate table of pins.** **Recommended: columns on `PostalAddress`**: one address has one point; a pin table would need its own ownership, deletion and archive handling for nothing. - **Resolve on save or on press.** **Recommended: offline resolution on save** (as a company's location is), **network never**; a street-level geocode is a press. - **Postcode data.** Adopt GeoNames' postal-code export as a second offline file, next to the city table, with its notice and a THIRD-PARTY row (#579), or stay at town level. **Recommended: add it, optional**: it is the step between a city and a street that costs no request. Not verified: how well it covers the countries the postal-rules plugin handles; and the UK part carries a Royal Mail notice that has to be read before it is shipped (leave a country out rather than ship it unread). - **Department.** **Recommended: leave out here**, and say so; it follows #329's decision on departments. ## Builds on Issues #92 (several addresses, not unique across the instance), #306 (an address checked against its country), #329 (companies get addresses), #366, #579. #700 decides engine and precision; #701 is the precise point from Wikidata. ## Needed by #703 (several addresses, several pins). ## Done when - `tests/test_postal_addresses.py`: an address with a known town and country is resolved to a town-level point on save and a postcode-level point where the file has it; an unknown one stays unplaced without an error; a second owner never sees the first's pins; a contact's address is not pinned until asked. - `tests/test_company_map.py`: pins appear in the list as text and, where drawn, match it; an unpinned contact never appears; the person's own address never appears on the overview. - `tests/test_gdpr.py`: a contact's export carries its coordinates and erasing the contact removes them. `tests/test_export.py`: **the archive format goes up by one** (`src/postulo/core/export.py` `FORMAT_VERSION`: one more than whatever `FORMAT_VERSION` is when the commit lands (29 at `f09b6010b`); `tests/test_export.py:96` follows it) and the round trip keeps them; the candidate format (4) does not change, because addresses are not in the career block. - **Standing checklist for a new data point:** API (`ProfileOut` and the contact and company schemas: expose or deliberately omit, said in the schema), search (coordinates are not searched), CV print choice (#308: a pin never prints), the processing record (`src/postulo/core/gdpr.py`), `tests/test_wiki_surface.py` for any new plugin-surface name, `tests/test_page_coverage.py` and `tests/security/test_isolation_sweep.py` for any new page, strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349). From the list of improvements of 2026-10-02.
tiagoagueda added this to the 1.0.0 milestone 2026-10-03 01:59:12 +00:00
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#702
No description provided.