Skip to main content

geotiff2pmtiles: A Fast, Memory-Efficient GeoTIFF to PMTiles Converter in Pure Go

Pascal Spörri
Author
Pascal Spörri
I let AI help me build pure-Go tool that converts GeoTIFF files into PMTiles to integrate on the web. I decided to focus on speed, low memory usage, and zero system dependencies.

The Starting Point: ESA WorldCover
#

This project started with the ESA WorldCover dataset — a global land cover product at 10 m resolution based on Sentinel-1 and Sentinel-2 data. The full dataset consists of 2561 GeoTIFF files totaling 116 GB. I wanted to convert it into a single PMTiles archive for use in a web-based flight simulator.

PMTiles is a single-file tile archive format designed for the cloud. Instead of hosting millions of individual tile files or running a tile server, you store one .pmtiles file on any static hosting (e.g. S3). Clients fetch tiles via HTTP range requests. There is no server-side logic required, which makes deployment trivially simple.

I used the recommended tool rio-pmtiles, a Python-based tool built on top of Rasterio and GDAL. I let it run for over a week — and it never finished. Worse, the process consumed so much memory that I had to configure 512 GB of swap space just to keep it alive. Thus I got frustrated and I decided leverage Cursor to create a new tool based on Go.

GeoTiff2PMTiles
#

Two weeks working on a& nd off the project and 100$ in Opus 4.6 tokens later I have geotiff2pmtiles that does the whole conversion in 16 hours. I already had a working solution after a couple of hours but I spent some time fleshing out the project. The project supports multiple projections, GeoTIFF source types and a slew of resampling methods.

Most of the time spent computing the ESA-Worldview dataset is spent on zoom level 13 - the highest avilable resolution. With a tile size of 512 px an impressive 45 million tiles are computed.

Here’s how long each layer takes to process:

ZoomTilesThroughputDuration
1345,572,0961,069/s710m 14s
1211,395,0721,852/s102m 33s
112,850,8161,667/s28m 30s
10712,7041,504/s7m 53s
9178,6881,369/s2m 10s
844,8001,235/s36s
711,264995/s11s
Total21,211,68115h 49m 42s

I could have a tool that does the job after a single week, but I decided to add additional features and improve the performance.

ESA Worldcover Screenshot

Architecture
#

geotiff2pmtiles reads Cloud Optimized GeoTIFF (COG) tiles on demand, from disk, generates map tiles in parallel, and writes them directly into a PMTiles archive — all in a single pass.

Memory-Mapped I/O
#

Instead of loading entire GeoTIFF files into memory, the tool uses memory-mapped I/O (mmap) to access TIFF tiles on demand. The operating system manages paging — only the portions of the file currently being accessed reside in physical memory. This keeps the memory footprint bounded regardless of input file size.

Parallel Tile Generation
#

Tiles are grouped into Hilbert-contiguous batches of 32 and distributed across a configurable worker pool. This batch size balances spatial locality (good cache reuse) with load distribution (minimal tail imbalance). Each worker performs per-pixel inverse projection and bilinear or nearest-neighbor resampling from the cached COG tiles.

Pyramid Downsampling
#

Lower zoom levels are generated by combining 2x2 child tiles into parent tiles via box-filter downsampling, rather than re-reading from the source COGs. This avoids redundant I/O and resampling work. For elevation data (Terrarium encoding), the downsampler decodes RGB back to elevation values, averages valid samples, and re-encodes — ensuring correct interpolation even across tile boundaries.

Different downsampling modes are supported:

  • Bicubic (4×4 Catmull-Rom) - default
  • Mode (most common value) - ideal for categorical data
  • Bilinear
  • Nearest-neighbor
  • Lanczos-3

Profiling-Driven Optimizations
#

To achieve the desired performance I let Cursor run and profile the tool in a loop. It then decided to focus on what to work on and improve. Amongst other improvements forgotten I ended up with this list of optimizations:

OptimizationCPU beforeCPU afterImpact
Integer cache keys101s (18%)< 1sEliminated string hashing
Type-specific pixel reads57s (10%)0sEliminated interface boxing
Remove madvise48s (9%)0sEliminated redundant syscalls
Per-tile source/level prep37s (6%)< 1sEliminated per-pixel recomputation
Batch work queueImproved parallelism from 3.2× to 3.3×
Total594s CPU, 188s wall248s CPU, 81s wall2.3× faster

I also got this nice summary how the rest of the time is spent:

The remaining CPU time is dominated by fundamental work: page faults for reading mmap’d COG data (19%), writing output pixels (16%), and YCbCr→RGB color conversion (14%). These represent the actual cost of decoding JPEG tiles and compositing output images — not overhead.

Pretty impressive, right? I should probably re-check the performance since I added a lot of features which I haven’t benchmarked yet.

Let’s see if I can find a global satellite data set.

Source Code
#

The project is open source and available on GitHub: pspoerri/geotiff2pmtiles.

Pre-built binaries for Linux and macOS (amd64/arm64) are available on the releases page.