Work / Tool Prototype
Urban Atlas Studio: print-ready city map posters from open map data
Urban Atlas Studio turns OpenStreetMap data into print-ready city map posters: 26 themes, four layouts and three approved sizes, drawn as vector PDFs to the exact size a print-on-demand product needs. The engine is deterministic, and it refuses a size it cannot tie to a real product.
Demonstration data. Public map data with invented poster text and an invented date.
One command turns a stretch of Galway’s coast into a poster file ready to print at 30 by 40 centimetres.
urbanatlas render "Galway, Ireland" --lat 53.2707 --lon -9.0568 --theme crimson_coast \
--size 30x40 --layout panel --span 5 --subtitle Ireland --proof
Galway_crimson_coast_30x40_panel.pdf
roads 2,034 · water 50 · parks 1,046 · sea 1
trim 300x400 mm · bleed 308x408 mm · sRGB intent

At a glance
- Status: Prototype.
- Platform: a Python command-line tool, with a Claude skill that can drive it.
- Stack: Python, osmium for OpenStreetMap files, GeoPandas and Shapely for the geometry, Matplotlib for drawing, pikepdf for the PDF page boxes.
- Built with: Claude Code and Codex.
- AI inside the product: none. The engine calls no model. A Claude skill can run the commands for me, but the posters are drawn by ordinary code.
What it makes
Posters for an Etsy shop fulfilled by Gelato, which prints and posts on demand. The engine produces the part that has to be right before any money is involved, the artwork.
There are 26 themes, and I decided they all ship. Here is one place in six of them.

Four layouts cover the formats: a full-bleed map with no type, a map inset in a margin with the city name and coordinates beneath, a map cropped to a disc, and a personal layout with a headline, a date and a pin.

The same engine draws other cities from the regions I have ingested: Ireland and Northern Ireland, the Paris region, Tokyo’s Kanto region and Catalonia.

An engine that refuses to guess
Real print costs sit downstream of every file, so the engine is built to say no. Ask for a size that does not match a product and it stops:
urbanatlas render "Galway, Ireland" ... --size 45x60
error: "'45x60' is not an approved poster size. Valid sizes: 30x40, 50x70, 70x100.
Sizes must correspond to a verified Gelato product - the product determines the
artwork, not the other way round."
Build decision. Sizes come from a short approved list, each tied to a print product, not from whatever number I type. Two more sizes exist in the code but are held out of the launch catalogue until I decide the listing structure.
Two more decisions shaped the output file.
Exactly the size asked for. The prototype let the plotting library size the file from its contents, so a request for 3600 by 4800 pixels produced 3630 by 4830. For a manufacturing file that is a wrong-sized product. The rebuild lays the page out in millimetres, so a 12 mm margin is 12 mm at every poster size, and it writes the trim and bleed boxes into the PDF.
Vector glow, not a bloom. The night-lights theme gets its glow from layered vector strokes. A raster bloom would look more photographic, but it would force the print file to be a picture, and a vector file is what lets one design print at 30 by 40 and at 70 by 100.
A contrast check keeps themes readable, and I recalibrated its thresholds against rendered output because brightness alone cannot judge colour separation. Every theme still gets a look from me before it ships.
Under the hood
I built Urban Atlas Studio with Claude Code and Codex as a Python command-line tool, designing the product, the themes and the print rules myself.
Map data comes from regional extracts downloaded on demand and ingested once per region, because the file reader has no spatial index and slicing a small box still means reading the whole country. Ingested features are cached by bounding box, and a request that fits inside a cached box reuses it. Sea comes from a separate prepared coastline dataset, because OpenStreetMap stores a coastline as a line and not as an area.
| Layer | What I used |
|---|---|
| Interface | A Python command-line tool, plus a Claude skill that can run the commands |
| Language | Python |
| Map data | OpenStreetMap regional extracts read with osmium, ingested once per region and cached by bounding box |
| Geometry and drawing | GeoPandas and Shapely for geometry, Matplotlib for drawing, laid out in millimetres |
| Output | Vector PDF with trim and bleed boxes written by pikepdf, an sRGB colour intent, and an optional PNG proof |
| Runtime AI | None |
| Tests | 359 tests |
| Fulfilment | Etsy shop with Gelato printing on demand |
The test suite has 359 tests.

What it shows
Urban Atlas Studio is one engine with one deterministic path from open data to a file a printer can use at its exact size, with an approved list standing between me and a wrong-sized product. The same engine draws a coastline in Galway, a river in Paris and a personal poster from a pin on a map, in 26 themes and four layouts, and every size it will accept is tied to a real print product. That is the part of a print-on-demand shop that cannot be allowed to drift, and it is the part I have made dependable.