Open-source project
originalankur/maptoposter avatar
originalankur/maptoposter

MapToPoster: one script, a geocoded centre point and 17 theme JSON files

Project brief: Transform your favorite cities into beautiful, minimalist designs. MapToPoster lets you create and export visually striking map posters with code.

14,299 stars1,253 forksPythonMIT

At a glance

What is it?
maptoposter is a single file Python CLI that geocodes a city name, draws its street network and writes one themed PNG. It fits someone who wants a printable map poster from one command, and it stops well short of cartography: fixed output folder, one city per run, no token, and two dependency files that have already drifted apart.
Who is it for?
Adopt maptoposter when you want one decorative map of one named city per command and you are willing to edit a theme JSON file. Skip it when you need an output path other than posters/, a batch of cities in a single run, or anything larger than 20 inches on a side.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 100 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • originalankur/maptoposter14,299 ★ · +415
Star history
Repository2026-08-042026-11-13
originalankur/maptoposter13.9k14.3k

415 Stars in the last 101 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

create_map_poster.py plus 17 theme JSON files is the whole tool

MapToPoster is one Python entry point, create_map_poster.py, with font_management.py beside it and 17 JSON files in themes/. You name a city and a country, the script resolves that place, draws its street network, and writes a single PNG into posters/. Nothing listens on a port, and there is no configuration file to edit before the first run.

The design surface is small: a theme name, a radius in metres, two sizes in inches, and a few text overrides. A theme is plain JSON rather than Python, so copying terracotta, the default, and editing it is how you get a palette the repository does not ship. The custom theme example opens with name, description, bg and text, which tells you the first four keys to copy. One theme name selects the whole look, so that file is where every visual decision lives.

That is also where the difference from a general desktop GIS such as QGIS shows up. QGIS lets you assemble layers and annotate them yourself, and it asks you to build the project before you get a picture. maptoposter hands you a finished PNG from one command and takes the styling decisions away from you in exchange.

The city name is geocoded first, and the image gives you no way to tell it went wrong

Every poster starts with a name lookup, and that lookup decides what you get. In the flag table --city and --country are described as used for geocoding, nothing more. Whatever coordinates come back become the centre of the drawn area, so the finished map is a consequence of how the geocoder read your two strings.

That is the first failure mode, and it is invisible once it happens. A country the geocoder does not accept, or a city name shared by several places, shifts the centre and you get a convincing poster of the wrong place with nothing printed on it to say so. --latitude and --longitude are the manual correction, and the table pairs them explicitly, describing each as something to use with the other. The flag table gives no error text for a half supplied pair, so pass both or neither.

--country-label handles the separate problem of wording. Geocoding input and printed country text are independent, which lets you look up a short name and print a longer one on the poster.

18,000 metres is the default radius, and it is too wide for most of the cities in the examples

--distance defaults to 18000 metres, a value that fits a metro with a wide footprint and little else. The distance guide splits the choice into bands: 4000 to 6000 m for small dense cities such as Venice or central Amsterdam, 8000 to 12000 m for a focused downtown like Paris or Barcelona, 15000 to 20000 m for a full view of Tokyo or Mumbai. Radius decides legibility before a theme is ever applied.

The complete call fits on one line:

bash
python create_map_poster.py -c "New York" -C "USA" -t noir -d 12000

Sizes work the same way. --width defaults to 12 inches and --height to 16, each capped at 20, and the resolution guide converts inches to pixels at 300 DPI: 3.6 x 3.6 for an Instagram post, 6.4 x 3.6 for an HD wallpaper, 12.8 x 7.2 for 4K, 8.3 x 11.7 for A4. At that density the default poster is 3600 x 4800 pixels, and 20 inches is the hard ceiling on each side. Beyond it you need another pipeline.

Non-Latin labels pull a Google Fonts file on first use, with no glyph coverage check

Non-Latin text is handled by overriding what the poster prints and naming a font that carries the characters. --display-city and --display-country replace the two names, and --font-family takes a Google Fonts family name:

bash
python create_map_poster.py -c "Tokyo" -C "Japan" -dc "東京" -dC "日本" --font-family "Noto Sans JP" -t japanese_ink

Fonts are downloaded from Google Fonts and cached locally in fonts/cache/, so the first render in each script pays for that download and later runs read the cache. Two gaps matter. Nothing in the documented flags verifies that the family covers the characters you passed.

uv run is the fast path, pip installs a second dependency list

The documented first path uses uv, which builds the environment on the first run:

bash
# First run will automatically install dependencies
uv run ./create_map_poster.py --city "Paris" --country "France"

# Or sync dependencies explicitly first (using locked versions)
uv sync --locked
uv run ./create_map_poster.py --city "Paris" --country "France"

The first command needs uv and nothing else. The second resolves the exact versions recorded in uv.lock before rendering, which is the reproducible one.

If you would rather own the interpreter yourself, the repository also ships a plain requirements.txt:

bash
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -r requirements.txt

Run the script from that environment with python in front of the filename:

bash
python create_map_poster.py -c "Paris" -C "France"

What lands on disk is one PNG in posters/ named with the city, the theme and a timestamp, plus a fonts/cache/ directory that fills the first time each family is used.

requirements.txt carries five lint packages that pyproject.toml leaves out

Two dependency lists ship in the repository and they are not the same list. pyproject.toml names the project maptoposter at version 0.2.0, requires Python >=3.11, and pins 30 runtime packages from certifi==2026.1.4 through urllib3==2.6.3, including the heavy geospatial set: geopandas==1.1.2, osmnx==2.0.7, shapely==2.1.2, scipy==1.16.3.

requirements.txt repeats that set and adds five packages no render needs: flake8==7.3.0, pycodestyle==2.14.0, pyflakes==3.4.0, mccabe==0.7.0 and types-requests==2.32.4.20260107. It also spells one dependency differently, writing lat_lon_parser where pyproject writes lat-lon-parser. The root .flake8 config explains the extra entries: that tooling belongs to the project's own checks.

The cost of the pip path is that a linter and its plugins sit in the same interpreter that draws your poster. The cost that will bite later is drift. Nothing ties the two files together, so a version bump lands in pyproject.toml and uv.lock while requirements.txt stays behind until someone reads it. Pin your environment with `uv sync --locked` rather than with the requirements file. The >=3.11 floor means an older interpreter fails at install on either path, which is at least a clear message instead of a broken render.

One city per process, one folder you cannot redirect, and flags that change nothing but text

The optional flag table has nine entries and none of them sets an output path, a file name or a working directory. Output always lands in posters/, named {city}_{theme}_{YYYYMMDD_HHMMSS}.png, so batch work is a shell loop and every pass repeats the geocoding and the map download for its own city.

The two flags that look like batch switches vary something other than the place. --list-themes prints the 17 names, and --all-themes renders one centre in every theme, which means 17 PNGs for a single city rather than one PNG for 17 cities. Because the timestamp has one second resolution and the name carries no coordinates, two runs of the same city and theme inside the same second overwrite each other without warning, and nothing in the documentation tells you to empty posters/ afterwards.

Only three flags change the words on the image: --country-label, --display-city and --display-country. Everything else moves the view or the palette, so if your poster needs a caption, a credit line or a different label for one neighbourhood, that text has to be added in an image editor after the render.

Version 0.2.0 exists only in pyproject.toml, and the packaging metadata declares one module

The repository has no GitHub releases, so there is no tag to install and no release note to diff a version against. Version 0.2.0 appears in pyproject.toml. A CHANGELOG.md sits at the root, and the last push was on 2026-08-06, so the tree keeps changing without a release wrapping it. Pin a commit if you need to repeat a poster exactly, because uv.lock freezes the dependencies but not the script that calls them.

There is a packaging wrinkle to know before you build anything from this metadata. The build section requires setuptools>=68 and wheel, and [tool.setuptools] sets py-modules = ["create_map_poster"]. Only one module is declared, so font_management.py is not packaged, and the themes/ directory holding the JSON the script needs at run time has no data file entry in that file. A wheel built from this metadata would arrive without the font helper or the themes. Both documented install paths run from a clone, which sidesteps the question until you want a version you can put in a container.

Editorial conclusion

Adopt maptoposter when you want one decorative map of one named city per command and you are willing to edit a theme JSON file. Skip it when you need an output path other than posters/, a batch of cities in a single run, or anything larger than 20 inches on a side. Before the first render, run --list-themes, then confirm the geocoded centre with --latitude and --longitude, because every crop, label position and road density downstream depends on that one point.

Frequently asked questions

How do you use maptoposter?

Run create_map_poster.py from a command line with two required arguments, --city and --country, which are used for geocoding. Optional flags cover the theme, the radius in metres, the size in inches and the text printed on the poster, and fonts requested through --font-family are cached under fonts/cache/ after the first download.

Does maptoposter need an API key or a map token?

No flag in the documented table takes a key or a token, and the only required input is a city name plus a country name, optionally replaced by --latitude and --longitude. Outbound network access is still needed on the first run, because fonts come from Google Fonts.

How do I make a 4K wallpaper or an A4 print with maptoposter?

Pass inches to --width and --height, which default to 12 and 16 and are capped at 20 each. The guide maps those inches to pixels at 300 DPI, giving 12.8 x 7.2 for 3840 x 2160 and 8.3 x 11.7 for 2480 x 3508 A4.

Where does maptoposter save the PNG, and can I choose the folder?

Files always go to posters/ with the name {city}_{theme}_{YYYYMMDD_HHMMSS}.png. None of the documented optional flags sets a path or a filename, so moving the output means copying the file or editing the script.

Official sources

  1. Official README
  2. Project repository
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/originalankur-maptoposter.svg)](https://hysenlabs.com/projects/originalankur-maptoposter)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/originalankur-maptoposter.svg)](https://hysenlabs.com/projects/originalankur-maptoposter)

Was this useful?