Skip to content

Instantly share code, notes, and snippets.

@glostis
Created March 12, 2026 16:23
Show Gist options
  • Select an option

  • Save glostis/31a0d9d79597373d18912200c80ba5cc to your computer and use it in GitHub Desktop.

Select an option

Save glostis/31a0d9d79597373d18912200c80ba5cc to your computer and use it in GitHub Desktop.
Creating a fully offline world vector basemap for QGIS

Creating a fully offline world vector basemap for QGIS

This document describes how to generate a fully offline world vector basemap for QGIS.

1. Generating the basemap

The Protomaps project offers a convenient way to generate a vector basemap from OpenStreetMap data using the Protomaps CLI.

A full planet file is roughly 120 gigabytes, including zoom levels from 0 to 15. Each additional zoom level roughly doubles the size of the file.

You probably don't need data with such a level of detail, so you can use the extract CLI command to create a file of the size of your choice by adjusting the maximum zoom level:

Zoom level File size Level of detail
6 40 MB Regions, major roads
7 180 MB
8 500 MB Secondary roads
9 1.5 GB
10 3.5 GB Major streets
11 7.5 GB
12 17 GB
13 32 GB Buildings

Example with level 8:

docker run -v $(pwd):/data protomaps/go-pmtiles extract https://build.protomaps.com/20260206.pmtiles --maxzoom 8 /data/world-20260206-maxzoom8.pmtiles

2. Converting the basemap to a QGIS-compatible format

QGIS unfortunately can't load .pmtiles files directly1, so we need to convert it to a vector tile format that QGIS natively handles: MBTiles.

This can be done using the tile-join command of tippecanoe2.

Example with the level 8 PMTiles previously generated:

docker run -v $(pwd):/data --entrypoint /usr/local/bin/tile-join versatiles/versatiles-tippecanoe -o /data/world-20260206-maxzoom8.{mbtiles,pmtiles}

This command can take a while to run, be patient! It will generate a file of roughly the same size as the PMTiles file.

3. Styling the basemap

We now have a vector basemap .mbtiles file that can be loaded in QGIS, but it's not styled yet.

The Protomaps project provides MapLibre GL styles in various "flavors" that can be used to style their PMTiles basemaps.

As explained in the docs, their styles can be exported as JSON files by choosing the flavor and clicking on Get style JSON in maps.protomaps.com. The white or black flavors can be good choices for a basemap.

The JSON then needs to be tweaked in order for text labels to be rendered correctly in QGIS3:

import json

target_ids = [
    "places_country",
    "places_locality",
    "places_region",
    "places_subplace",
    "roads_labels_major",
    "water_label_lakes",
    "earth_label_islands",
    "water_label_ocean",
    "roads_labels_minor",
    "water_waterway_label",
]

input_file = "./style.json"

with open(input_file) as f:
    style_data = json.load(f)

for layer in style_data["layers"]:
    if "id" in layer and layer["id"] in target_ids:
        if "layout" in layer and "text-field" in layer["layout"]:
            layer["layout"]["text-field"] = ["get", "name"]

with open(input_file, "w") as f:
    json.dump(style_data, f, indent=2)

4. Loading the basemap in QGIS

Drag and drop the .mbtiles file in QGIS, then apply the style JSON file to the layer:

  1. Double click on the layer to open its properties
  2. Go to the Symbology tab
  3. Click on the Style > Load Style menu button
  4. Select the JSON file and click Load Style

That's it!

Footnotes

  1. PMTiles can currently be loaded in QGIS if served by a tile server (e.g. with the CLI pmtiles serve). It may be possible to load them directly in QGIS in the future. See PMTiles issue for more information

  2. This conversion could maybe also be done with GDAL/OGR? I haven't tried.

  3. This is because QGIS uses a different syntax for text label filters than MapLibre GL. The style.json file provided by Protomaps uses a syntax with complex filters with fallbacks on names in different languages that QGIS can't handle. The python script simply replaces these filters with the default name of the features.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment