Companies on a map — without telling a tile server where somebody is applying #108

Closed
opened 2026-09-07 16:43:02 +00:00 by tiagoagueda · 2 comments
Owner

Observation

target to 0.5.0 show companies on a map

Fits the milestone: 0.5.0 is when Postulo stops being a European application, and a map is
the screen where that becomes obvious.

Postulo does not know where anything is

location = models.CharField(_("location"), max_length=200, blank=True)

Free text, on both Company and JobPosting. "Lisboa, Portugal", "Remote", "Berlin (hybrid)",
"" — whatever somebody typed or whatever a capture read off a page. There are no coordinates
anywhere in the schema, so the first half of this issue is geocoding, not drawing.

Two ways, and they are not close:

  • A geocoding service. Nominatim is the obvious one and it has a usage policy: attribution,
    a real User-Agent (Postulo has one), no bulk, no heavy use. Every instance geocoding its
    own companies is not bulk, so this is legitimate — but it is a request to somebody else's
    server carrying the name of a company this person is applying to, which is precisely the
    disclosure the rest of this codebase refuses to make.
  • An offline dataset. GeoNames' cities file is a few megabytes for everywhere above a
    thousand inhabitants, and free-text like "Lisboa, Portugal" is a city and a country, which
    is exactly what it resolves. Nothing leaves the instance. It cannot place a street address,
    which for this purpose is a feature rather than a limitation — see below.

Tiles are the hard part, and this project has already refused the pattern once

A map is not a picture; it is dozens of requests to a tile server, one per square of what you
are looking at. Those requests describe your viewport, and the viewport is centred on the
places somebody is applying to.

That is the same disclosure jobs/logos.py exists to prevent, only sharper:

An <img> pointing at somebody else's server would tell them, on every page view, which
companies this person is applying to and when they looked.

The policy already says no in two directions at once — img-src 'self', data: blocks raster
tiles and connect-src 'self' blocks vector ones — and that is not an obstacle to work around
either. Three honest ways through:

  1. No basemap. Plot the points on a plain world outline shipped with Postulo, a few
    kilobytes of SVG. No tiles, no requests, no provider, nothing to attribute. It answers
    where am I applying — which is the actual question — and answers nothing about street
    layouts, which nobody asked. Recommended, and it is the only option that needs no
    further decisions.
  2. Postulo proxies the tiles. Works with img-src 'self', and the tile provider sees the
    instance rather than the person. But the operator's box becomes a tile cache with the
    bandwidth and storage that implies, and somebody still has to pick a provider and honour
    its terms.
  3. Self-hosted tiles. A country extract is tractable, the planet is not, and either way
    it is a second service to run beside Postulo. For an application whose whole premise is
    that it fits on the machine somebody already has, this is a lot.

Precision is a decision, not a detail

City level, deliberately. A map of somebody's job search at street precision is a map of
where they will be at nine in the morning if any of it works out, sitting in a database
alongside their CV and their salary expectations. docs/THREAT-MODEL.md should have a line
about it.

City level also happens to be all the data supports — location is usually a city, and an
offline city dataset resolves exactly that — so the private answer and the accurate answer
are the same one.

The map is the enhancement; the list is the feature

Two of this project's promises land squarely on this screen.

A map is the classic screen-reader failure. So the page is a list of places with the
companies at each, and the map draws the same data beside it. The list is not a fallback, it
is the page; somebody using a keyboard or a screen reader gets the whole answer, and the axe
sweep has something to sweep. Every marker's information exists in text before any of it is
drawn.

And it has to work with scripts off, like the rest. Which rules out an interactive library
as the only rendering: either the server draws an SVG with the points on it, or the page is
the list and the map appears when a script can run.

What it is actually for

Worth being honest, because it changes what gets built. Somebody applying in one city gets one
dot and learns nothing. The map earns its place for a search spread across places: comparing
where the interest is, noticing that everything is in one expensive city, seeing a cluster
worth relocating for. That argues for counts by place over pins — a proportional dot per
city, not a marker per company — which is also less to draw and less to place precisely.

Scope

  • Coordinates on Company, nullable, with where they came from and when — a geocode is a
    guess and the record should say so.
  • Resolution from an offline dataset, run when a location is saved and never in the
    background, the rule capture already follows: "Postulo makes no request nobody asked for."
  • A place list, and a map drawn from the same queryset.
  • Somewhere to correct a wrong guess by hand, because "Springfield" is ambiguous and
    "Remote" is not a place at all.
  • Whatever dataset ships gets a line in TRADEMARKS.md and its licence beside it, the way
    the flags do.

Classification

Enhancement, interface. Not blocked by anything, and the geocoding half is the larger one.

## Observation > target to 0.5.0 show companies on a map Fits the milestone: 0.5.0 is when Postulo stops being a European application, and a map is the screen where that becomes obvious. ## Postulo does not know where anything is ```python location = models.CharField(_("location"), max_length=200, blank=True) ``` Free text, on both `Company` and `JobPosting`. "Lisboa, Portugal", "Remote", "Berlin (hybrid)", "" — whatever somebody typed or whatever a capture read off a page. There are no coordinates anywhere in the schema, so **the first half of this issue is geocoding**, not drawing. Two ways, and they are not close: - **A geocoding service.** Nominatim is the obvious one and it has a usage policy: attribution, a real `User-Agent` (Postulo has one), no bulk, no heavy use. Every instance geocoding its own companies is not bulk, so this is legitimate — but it is a request to somebody else's server carrying **the name of a company this person is applying to**, which is precisely the disclosure the rest of this codebase refuses to make. - **An offline dataset.** GeoNames' cities file is a few megabytes for everywhere above a thousand inhabitants, and free-text like "Lisboa, Portugal" is a city and a country, which is exactly what it resolves. Nothing leaves the instance. It cannot place a street address, which for this purpose is a feature rather than a limitation — see below. ## Tiles are the hard part, and this project has already refused the pattern once A map is not a picture; it is dozens of requests to a tile server, one per square of what you are looking at. **Those requests describe your viewport**, and the viewport is centred on the places somebody is applying to. That is the same disclosure `jobs/logos.py` exists to prevent, only sharper: > An `<img>` pointing at somebody else's server would tell them, on every page view, which > companies this person is applying to and when they looked. The policy already says no in two directions at once — `img-src 'self', data:` blocks raster tiles and `connect-src 'self'` blocks vector ones — and that is not an obstacle to work around either. Three honest ways through: 1. **No basemap.** Plot the points on a plain world outline shipped with Postulo, a few kilobytes of SVG. No tiles, no requests, no provider, nothing to attribute. It answers *where am I applying* — which is the actual question — and answers nothing about street layouts, which nobody asked. **Recommended**, and it is the only option that needs no further decisions. 2. **Postulo proxies the tiles.** Works with `img-src 'self'`, and the tile provider sees the instance rather than the person. But the operator's box becomes a tile cache with the bandwidth and storage that implies, and somebody still has to pick a provider and honour its terms. 3. **Self-hosted tiles.** A country extract is tractable, the planet is not, and either way it is a second service to run beside Postulo. For an application whose whole premise is that it fits on the machine somebody already has, this is a lot. ## Precision is a decision, not a detail **City level, deliberately.** A map of somebody's job search at street precision is a map of where they will be at nine in the morning if any of it works out, sitting in a database alongside their CV and their salary expectations. `docs/THREAT-MODEL.md` should have a line about it. City level also happens to be all the data supports — `location` is usually a city, and an offline city dataset resolves exactly that — so the private answer and the accurate answer are the same one. ## The map is the enhancement; the list is the feature Two of this project's promises land squarely on this screen. **A map is the classic screen-reader failure.** So the page is a list of places with the companies at each, and the map draws the same data beside it. The list is not a fallback, it is the page; somebody using a keyboard or a screen reader gets the whole answer, and the axe sweep has something to sweep. Every marker's information exists in text before any of it is drawn. **And it has to work with scripts off**, like the rest. Which rules out an interactive library as the only rendering: either the server draws an SVG with the points on it, or the page is the list and the map appears when a script can run. ## What it is actually for Worth being honest, because it changes what gets built. Somebody applying in one city gets one dot and learns nothing. The map earns its place for a search spread across places: comparing where the interest is, noticing that everything is in one expensive city, seeing a cluster worth relocating for. That argues for **counts by place** over pins — a proportional dot per city, not a marker per company — which is also less to draw and less to place precisely. ## Scope - Coordinates on `Company`, nullable, with where they came from and when — a geocode is a guess and the record should say so. - Resolution from an offline dataset, run when a location is saved and never in the background, the rule capture already follows: *"Postulo makes no request nobody asked for."* - A place list, and a map drawn from the same queryset. - Somewhere to correct a wrong guess by hand, because "Springfield" is ambiguous and "Remote" is not a place at all. - Whatever dataset ships gets a line in `TRADEMARKS.md` and its licence beside it, the way the flags do. ## Classification Enhancement, interface. Not blocked by anything, and the geocoding half is the larger one.
tiagoagueda added this to the 0.5.0 milestone 2026-09-07 16:43:02 +00:00
Author
Owner

Superseded by #700: "street precision is not a column to keep" no longer holds for an address a person deliberately places, and "no tiles" no longer holds when the operator provides a tile source. Still standing: city level by default, no geocoding service in core, and the server-drawn SVG overview.

Superseded by #700: "street precision is not a column to keep" no longer holds for an address a person deliberately places, and "no tiles" no longer holds when the operator provides a tile source. Still standing: city level by default, no geocoding service in core, and the server-drawn SVG overview.
Author
Owner

Superseded in part on 2026-10-12: the map is drawn by Leaflet over OpenStreetMap tiles by default (#898), reversing #700's tile decision. City-level placement by default and the list as the page still stand.

Superseded in part on 2026-10-12: the map is drawn by Leaflet over OpenStreetMap tiles by default (#898), reversing #700's tile decision. City-level placement by default and the list as the page still 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#108
No description provided.