<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Protomaps Blog</title><link>https://protomaps.com/blog/</link><description>Recent content on Protomaps Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 16 Dec 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://protomaps.com/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>New CLI feature: pmtiles merge</title><link>https://protomaps.com/blog/pmtiles-merge-part-1/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/pmtiles-merge-part-1/</guid><description>&lt;div style="text-align: center">
 &lt;img src="cover.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The &lt;a href="https://github.com/protomaps/go-pmtiles">&lt;code>pmtiles&lt;/code> CLI&lt;/a> now includes &lt;a href="https://docs.protomaps.com/pmtiles/cli#merge">the highly requested &lt;code>merge&lt;/code> command&lt;/a> for combining multiple tilesets into a single archive.&lt;/p>
&lt;p>As an example use case: the &lt;a href="https://mapterhorn.com/">Mapterhorn&lt;/a> project distributes several archives:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://mapterhorn.com/data-access/">&lt;code>planet.pmtiles&lt;/code>&lt;/a> for the full planet terrain mosaic, from zoom levels 0 to 12.&lt;/li>
&lt;li>&lt;code>6-20-28.pmtiles&lt;/code>, &lt;code>6-21-29.pmtiles&lt;/code>, etc for zoom 13 and above in specific areas. These archives can contain up to zoom level 17.&lt;/li>
&lt;/ul>
&lt;p>These archives are split to ensure each one is at most a few hundred gigabytes: the total size of all tilesets is over 2 TB.&lt;/p>
&lt;p>If you need to combine both low resolution and high resolution tiles into a single archive for one area like Interlaken, you can accomplish this now with &lt;code>pmtiles extract&lt;/code> followed by &lt;code>pmtiles merge&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>pmtiles extract \
 --bbox=7.74,46.60,7.96,46.75 \
 https://download.mapterhorn.com/planet.pmtiles \
 planet.pmtiles

pmtiles extract \
 --bbox=7.74,46.60,7.96,46.75 \
 https://download.mapterhorn.com/6-33-22.pmtiles \
 6-33-22.pmtiles

pmtiles merge planet.pmtiles 6-33-22.pmtiles interlaken.pmtiles
&lt;/code>&lt;/pre>&lt;p>This results in a single archive, hostable as a single file on cloud storage, enabling smooth zoom from 0 to 17.&lt;/p>
&lt;h2 id="requirements">Requirements&lt;/h2>
&lt;p>The &lt;code>merge&lt;/code> command currently works for both raster and vector tilesets.&lt;/p>
&lt;ul>
&lt;li>All input archives must be &lt;a href="https://protomaps.com/blog/pmtiles-cluster/">clustered&lt;/a>.&lt;/li>
&lt;li>All input archives must be the same tile type. It&amp;rsquo;s not possible to combine raster with vector tiles, or PNG with JPEG, for example.&lt;/li>
&lt;li>All input archives must have the same compression method.&lt;/li>
&lt;li>For now, &lt;strong>inputs must be disjoint with 0 overlapping tiles&lt;/strong>. The easiest way to satisfy this is with non-overlapping zoom levels.&lt;/li>
&lt;/ul>
&lt;h2 id="disjoint-inputs">Disjoint inputs&lt;/h2>
&lt;p>The current implementation of &lt;code>pmtiles merge&lt;/code> does not work with overlapping inputs. The PMTiles CLI is a tool for reading and writing with general tile archives, not a tool for editing individual tiles.&lt;/p>
&lt;p>Consider two tilesets that contain zoom 0 to zoom 12 of two different regions of the world, like a square region around Tokyo and Paris. All tilesets that form a complete pyramid contain tile &lt;code>0,0,0&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code> 0, 0, 0 0, 0, 0 
┌───────────────┐ ┌───────────────┐
│ ┌───┐ │ │ │
│ │ P │ │ │ │
│ └───┘ │ │ │
│ │ │ ┌───┐│
│ │ │ │ T ││
│ │ │ └───┘│
└───────────────┘ └───────────────┘
&lt;/code>&lt;/pre>&lt;p>The logical merging of the two tilesets should result in a brand new &lt;code>0,0,0&lt;/code> tile combining the geometry (vector), or compositing the pixels (raster):&lt;/p>
&lt;pre tabindex="0">&lt;code> 0, 0, 0 
┌───────────────┐
│ ┌───┐ │
│ │ P │ │
│ └───┘ │
│ ┌───┐│
│ │ T ││
│ └───┘│
└───────────────┘
&lt;/code>&lt;/pre>&lt;p>However, combining geometry or image compositing operations are both outside the current scope of the PMTiles CLI.&lt;/p>
&lt;h2 id="recommendations">Recommendations&lt;/h2>
&lt;p>If your project requires merging different tilesets that are overlapping, it&amp;rsquo;s recommended to instead &lt;strong>pre-generate or extract the tilesets from the source data.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>For raster archives, use the &lt;a href="https://docs.protomaps.com/pmtiles/create#geotiff">rio-pmtiles&lt;/a> command line tool to generate a PMTiles directly from a GeoTIFF or other GDAL-readable source.&lt;/li>
&lt;li>For vector basemaps, use &lt;a href="https://docs.protomaps.com/pmtiles/cli#extract">&lt;code>pmtiles extract&lt;/code>&lt;/a> with a multipolygon input.&lt;/li>
&lt;li>For other vector datasets, use &lt;a href="https://github.com/felt/tippecanoe">tippecanoe&lt;/a> on the combined source data directly or use a Planetiler profile like &lt;a href="https://docs.protomaps.com/basemaps/build">protomaps/basemaps&lt;/a> on combined source data.&lt;/li>
&lt;/ul>
&lt;h2 id="future-work">Future work&lt;/h2>
&lt;p>A future enhancement to &lt;code>pmtiles merge&lt;/code> will allow overlapping input tilesets &lt;strong>only in the case for vector tilesets with non-overlapping input layers.&lt;/strong>&lt;/p>
&lt;p>This is especially useful for combining overlay data like bike lanes or choropleths with basemap tiles into a single archive, which can halve the number of requests for a mapping application and speed up the map browsing experience.&lt;/p></description></item><item><title>Mapterhorn - Terrain for Web Mapping</title><link>https://protomaps.com/blog/mapterhorn-terrain/</link><pubDate>Tue, 02 Sep 2025 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/mapterhorn-terrain/</guid><description>&lt;div style="text-align: center">
 &lt;img src="mapterhorn-logo.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The Protomaps project is the &lt;a href="https://docs.protomaps.com/pmtiles/">PMTiles&lt;/a> format, its tooling, and a 120GB &lt;a href="https://docs.protomaps.com/basemaps/downloads">basemap&lt;/a> vector cartographic tileset created from &lt;a href="http://openstreetmap.org">OpenStreetMap&lt;/a> and other open data sources. PMTiles is not limited to storing vector data - it&amp;rsquo;s also used for raster data, like scans of historical paper maps.&lt;/p>
&lt;p>Mapping apps often don&amp;rsquo;t just need to show vectors of buildings, boundaries and places. Some apps need elevation data, since interesting places on Earth aren&amp;rsquo;t flat! The &lt;a href="https://mapterhorn.com">Mapterhorn&lt;/a> project fulfills this with a new independent open data product - it&amp;rsquo;s &lt;em>Protomaps for Terrain&lt;/em>.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="viewer.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;h2 id="project-inspiration">Project Inspiration&lt;/h2>
&lt;p>Mapterhorn&amp;rsquo;s inspiration is the &lt;a href="https://github.com/tilezen/joerd">Mapzen Joerd&lt;/a> project. Joerd is available as &lt;a href="https://aws.amazon.com/marketplace/pp/prodview-x7vtai3hasf26">tiles from AWS Open Data&lt;/a>, and was originally created for the &lt;a href="https://github.com/tangrams/tangram">Tangram&lt;/a> map renderer, but works with &lt;a href="https://maplibre.org/maplibre-style-spec/terrain/">MapLibre GL&lt;/a> as well. It&amp;rsquo;s built from a collection of digital elevation models (DEMs) processed into a single tileset using batch jobs on Amazon Web Services.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">type&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#e6db74">&amp;#39;raster-dem&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">tiles&lt;/span>&lt;span style="color:#f92672">:&lt;/span> [&lt;span style="color:#e6db74">&amp;#34;https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">maxzoom&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#ae81ff">13&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">encoding&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#e6db74">&amp;#39;terrarium&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">attribution&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;lt;a href=&amp;#39;https://github.com/tilezen/joerd/tree/master&amp;#39;&amp;gt;Joerd&amp;lt;/a&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mapterhorn&amp;rsquo;s goals are similar to Joerd - create a global, easy-to-use terrain tileset, with an initial focus on European DEMs.&lt;/p>
&lt;h2 id="key-project-differences">Key project differences&lt;/h2>
&lt;p>Mapterhorn&amp;rsquo;s design differs from Joerd and other open data projects in these ways:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Focus on interactive web visualization&lt;/strong> - The end product is sliced into tiles at conventional sizes like 512x512 pixels for direct usage in 2D and 3D web maps. Tiles are stored in the &lt;a href="https://github.com/tilezen/joerd/blob/master/docs/formats.md">&lt;code>terrarium&lt;/code> encoding&lt;/a> which MapLibre GL supports.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Ease of tileset recreation&lt;/strong> - Instead of being tied to AWS, Mapterhorn can be reproduced from scratch using a single powerful machine, either a desktop or a rented server. This means the pipeline can be customized with different data if your project requires more detail in certain countries. The full pipeline is &lt;a href="https://github.com/mapterhorn/mapterhorn">open source on GitHub.&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Ease of deployment&lt;/strong> - Mapterhorn distributes the end product as static PMTiles archives, which can be directly read from cloud storage to map libraries in browsers. This is used to visualize the Mapterhorn tileset on &lt;a href="https://mapterhorn.com/viewer/#map=10.5/47.1194/9.0788">mapterhorn.com&lt;/a> as well as the &lt;a href="https://pmtiles.io/#url=https%3A%2F%2Fdownload.mapterhorn.com%2Fplanet.pmtiles&amp;amp;map=0.45/0/0">pmtiles.io viewer&lt;/a>.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>Using the PMTiles format for distribution means you can use the &lt;code>pmtiles extract&lt;/code> CLI on a planet archive. To extract only the area surrounding the Matterhorn, try this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pmtiles extract &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> --bbox&lt;span style="color:#f92672">=&lt;/span>7.510659,45.897669,7.799642,46.04662 &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> https://download.mapterhorn.com/planet.pmtiles &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> planet.pmtiles
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="project-future">Project Future&lt;/h2>
&lt;p>&lt;a href="http://oliverwipfli.ch">Oliver Wipfli&lt;/a>, a former official coordinator of the MapLibre project is leading the development of Mapterhorn. The initial phases of the project are supported by an NLnet grant. If you&amp;rsquo;re a company or organization that needs high resolution terrain for web visualization, start a &lt;a href="https://github.com/mapterhorn/mapterhorn/discussions">discussion on GitHub&lt;/a>!&lt;/p></description></item><item><title>A Tour of the New pmtiles.io</title><link>https://protomaps.com/blog/new-pmtiles-io/</link><pubDate>Thu, 15 May 2025 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/new-pmtiles-io/</guid><description>&lt;p>&lt;a href="https://pmtiles.io">pmtiles.io&lt;/a> is an &lt;a href="https://github.com/protomaps/PMTiles/tree/main/app">open source&lt;/a> viewer for PMTiles tilesets. It&amp;rsquo;s a great way to inspect tilesets before you publish them on your site or app. It&amp;rsquo;s also a simple way to share GIS datasets without coding anything - just upload your PMTiles to any cloud storage, set up CORS, and deep link to the file on pmtiles.io using embedded URL parameters.&lt;/p>
&lt;p>pmtiles.io got a re-write! It now features:&lt;/p>
&lt;ul>
&lt;li>Light UI mode&lt;/li>
&lt;li>support for viewing &lt;a href="https://github.com/mapbox/tilejson-spec">TileJSON&lt;/a> (ZXY endpoints) in addition to PMTiles. For example, inspect the &lt;a href="https://pmtiles.io/#url=https%3A%2F%2Fvector.openstreetmap.org%2Fshortbread_v1%2Ftilejson.json&amp;amp;map=3.28/46.85/16.37">vector tiles hosted at vector.openstreetmap.org&lt;/a>.&lt;/li>
&lt;li>Drag-and-drop of local files&lt;/li>
&lt;li>Inspect raster tilesets&lt;/li>
&lt;/ul>
&lt;p>At the top of the site are three main tabs for &lt;strong>Map&lt;/strong>, &lt;strong>Archive&lt;/strong> and &lt;strong>Tile&lt;/strong>.&lt;/p>
&lt;h2 id="the-map-view">The Map view&lt;/h2>
&lt;p>The primary view for looking at tilesets and JSON metadata. Zoom in, pan around, and deep link to a specific view using URLs. Powered by &lt;a href="https://maplibre.org">MapLibre GL JS&lt;/a>. View vector features, color-coded by layer, labeled by their &lt;code>name&lt;/code> tag:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="mapview_1.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>Check &lt;code>Inspect Features&lt;/code> to hover an individual feature&amp;rsquo;s tags:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="mapview_2.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The popup also shows geographic coordinates of the cursor, and includes a button linking to the &lt;strong>Tile View&lt;/strong>.&lt;/p>
&lt;h2 id="the-tile-view">The Tile View&lt;/h2>
&lt;p>The Tile view is for map developers that need to drill down into the details of vector geometry. View a single tile in isolation and scale it up to see detailed geometry, without swapping in new tiles. It&amp;rsquo;s a simple SVG rendering of a single tile powered by &lt;a href="https://d3js.org">D3.js&lt;/a>.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="tileview_1.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>Use the top navigation to browse parent, child and neighbor tiles:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="tileview_2.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;h2 id="the-archive-inspector-view">The Archive Inspector View&lt;/h2>
&lt;p>The Archive Inspector is for implementers of the PMTiles specification, and visualizes the low-level internals of a tileset as a tree structure.&lt;/p>
&lt;p>Opening the inspector shows the root directory of the archive, the size of tiles or leaf directories, and summary statistics in the header.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="archiveview_1.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>Clicking a tile entry opens the &lt;strong>Tile View&lt;/strong>, and clicking a leaf entry opens another directory panel.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="archiveview_2.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>On the right, the current directory is visualized on a globe, with tile entries represented as squares, and run-length encoded duplicate tiles represented as space-filling curves.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="archiveview_3.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>You don&amp;rsquo;t need to have a tileset ready to try the new pmtiles.io - a variety of example datasets are included:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://pmtiles.io/#map=17.17/25.015092/121.525013&amp;amp;url=https%3A%2F%2Fdemo-bucket.protomaps.com%2Fv4.pmtiles">v4.pmtiles&lt;/a> - vector basemap, the daily build from OpenStreetMap.&lt;/li>
&lt;li>&lt;a href="https://pmtiles.io/#map=17.17/25.015092/121.525013&amp;amp;url=https%3A%2F%2Fair.mtn.tw%2Fflowers.pmtiles">flowers.pmtiles&lt;/a> - raster overlay, a zoom 20+ small area of aerial drone imagery.&lt;/li>
&lt;li>&lt;a href="https://pmtiles.io/#url=https%3A%2F%2Foverturemaps-tiles-us-west-2-beta.s3.amazonaws.com%2F2025-04-23%2Fplaces.pmtiles">places.pmtiles&lt;/a> - POIs derived from Meta and Microsoft places, distributed by the Overture Maps project.&lt;/li>
&lt;/ul></description></item><item><title>Covering the Planet with Compressed Bitmaps</title><link>https://protomaps.com/blog/pmtiles-compressed-bitmaps/</link><pubDate>Mon, 27 Jan 2025 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/pmtiles-compressed-bitmaps/</guid><description>&lt;p>This is the first post explaining how the &lt;a href="https://docs.protomaps.com/pmtiles/cli#extract">pmtiles extract&lt;/a> command works to slice a smaller tileset from a larger one. &lt;a href="https://maps.protomaps.com/builds/">Protomaps Builds&lt;/a> contains a daily planet tileset - if you want just Canada, or South America, you can pass &lt;code>pmtiles extract&lt;/code> a GeoJSON to get only that region.&lt;/p>
&lt;p>The first step in extracting a slice is computing a &lt;strong>tile covering&lt;/strong> for the target region. A &lt;em>covering&lt;/em> is the set of all tiles that touch the edge of the region, or lie completely within it. Coverings are also useful for finding if two tilesets overlap. If you&amp;rsquo;re &lt;strong>building tools for the PMTiles format&lt;/strong>, this covering technique can make your program faster and use less memory.&lt;/p>
&lt;h2 id="polygon-rasterization">Polygon Rasterization&lt;/h2>
&lt;p>Covering a region with tiles is the same task as &lt;a href="https://en.wikipedia.org/wiki/Rasterisation">polygon rasterization&lt;/a> in computer graphics. Start with a polygon like a triangle, then find all the pixels to fill the shape on a screen.&lt;/p>
&lt;p>Classical algorithms perform this with scanlines or other algorithms - see &lt;a href="https://alienryderflex.com/polygon_fill/">this interactive demo&lt;/a> to explore methods. For computer graphics, the output is a framebuffer of pixels.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="PolygonRasterization.png" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>Working with map tiles results in much larger effective screen sizes: the current maximum zoom of the Protomaps basemaps is 15, which is 32768 x 32768 pixels. A region covering for a continent where each entry is stored as (Z,X,Y) in a Set or Hashmap consumes 100+ megabytes of memory.&lt;/p>
&lt;h2 id="data-structure">Data Structure&lt;/h2>
&lt;p>Instead of holding a Set or Hashmap of pixels, we use a Bitmap - each &amp;ldquo;pixel&amp;rdquo; is a single boolean bit in a large array, like a black and white framebuffer image. We use the &lt;a href="https://protomaps.com/blog/blog/pmtiles-v3-hilbert-tile-ids/">Hilbert TileID&lt;/a> as a 64-bit integer index into the bitmap.&lt;/p>
&lt;p>Plain in-memory bitmaps are inefficient, though, because the domain of possible values is large: addressing zoom level 15 addresses over one billion tiles, and storing an array of zeros to set the (0,0) pixel bit at z15 would take up 100+ megabytes.&lt;/p>
&lt;p>&lt;a href="http://roaringbitmap.org">Roaring Bitmaps&lt;/a> are a popular implementation of &lt;strong>compressed bitmaps&lt;/strong>. These are especially good for storing regions on space-filling curves because areas occupy continuous parts of the curve, so can be run-length encoded by Roaring.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="RoaringBitmaps.png" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>Roaring Bitmaps are a core part of the software ecosystem around the PMTiles format, but not part of the format itself. It is cheap and easy to &lt;strong>construct an in-memory Roaring Bitmap&lt;/strong> over a set of PMTiles directory entries:&lt;/p>
&lt;pre tabindex="0">&lt;code>tile_id uint64, offset uint32, length uint32, run_length uint32
&lt;/code>&lt;/pre>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">roaring&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">Roaring&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">roaring&lt;/span>.&lt;span style="color:#a6e22e">addRange&lt;/span>(&lt;span style="color:#a6e22e">tile_id&lt;/span>, &lt;span style="color:#a6e22e">tile_id&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#a6e22e">run_length&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="algorithm">Algorithm&lt;/h2>
&lt;p>Roaring Bitmaps compactly represent tile coverings, but do not address how to efficiently compute those coverings.&lt;/p>
&lt;p>For the PMTiles Tile ID addressing scheme, we start with finding the boundary covering using classical methods, and then exploit the nature of the &lt;a href="https://en.wikipedia.org/wiki/Hilbert_curve">Hilbert curve&lt;/a>:
it has has the special property of &lt;strong>no jumps&lt;/strong> - unlike other addressing schemes such as &lt;a href="https://en.wikipedia.org/wiki/Z-order_curve">Morton (z-order)&lt;/a>, or Row/Column major, there are zero discontinuities in the Hilbert Curve. (The &lt;a href="https://en.wikipedia.org/wiki/Peano_curve">Peano curve&lt;/a> also shares this property.)&lt;/p>
&lt;p>This means that if the curve &lt;em>leaves&lt;/em> the region and later &lt;em>enters&lt;/em> the region, the intermediate positions on the curve between those two points are either &lt;strong>all outside&lt;/strong> or &lt;strong>all inside&lt;/strong>.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="hilbert.jpg" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>To fill in the interior polygon, iterate over the boundary bitmap and test every gap for whether or not the first tile lies on the interior of the polygon. If it does, add every tile up to the next boundary tile.&lt;/p>
&lt;h2 id="results">Results&lt;/h2>
&lt;p>The following table shows the growth in RAM usage of the program attached below, which covers the South American continent at different zoom levels using both techniques.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Zoom&lt;/th>
&lt;th>Hashmap (Memory)&lt;/th>
&lt;th>Hashmap (Time)&lt;/th>
&lt;th>Roaring (Memory)&lt;/th>
&lt;th>Roaring (Time)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>13&lt;/td>
&lt;td>54 MB&lt;/td>
&lt;td>150 ms&lt;/td>
&lt;td>3 MB&lt;/td>
&lt;td>7 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>14&lt;/td>
&lt;td>207 MB&lt;/td>
&lt;td>754 ms&lt;/td>
&lt;td>4 MB&lt;/td>
&lt;td>13 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>15&lt;/td>
&lt;td>823 MB&lt;/td>
&lt;td>3000 ms&lt;/td>
&lt;td>4 MB&lt;/td>
&lt;td>25 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>3295 MB&lt;/td>
&lt;td>24000 ms&lt;/td>
&lt;td>4 MB&lt;/td>
&lt;td>47 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>17&lt;/td>
&lt;td>13176 MB&lt;/td>
&lt;td>103000 ms&lt;/td>
&lt;td>5 MB&lt;/td>
&lt;td>95 ms&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For the simple hashmap representation of tile coordinates, the memory and runtime grow exponentially as Z increases, because each additional zoom level addresses 4x more tiles. At zoom level 17, it consumes a whopping 13GB of RAM and takes two minutes to compute.&lt;/p>
&lt;p>Roaring bitmaps and Hilbert IDs computes the same result in 100 milliseconds and stores the result in 5 megabytes, even at zoom level 17.&lt;/p>
&lt;h2 id="code">Code&lt;/h2>
&lt;p>A complete, runnable implementation of the compressed bitmap covering algorithm, with comparison to classical techniques, using the &lt;a href="https://github.com/paulmach/orb">paulmach/orb&lt;/a> library:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/RoaringBitmap/roaring/roaring64&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/paulmach/orb&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/paulmach/orb/geojson&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/paulmach/orb/maptile&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/paulmach/orb/maptile/tilecover&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/paulmach/orb/planar&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/paulmach/orb/project&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/protomaps/go-pmtiles/pmtiles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;io/ioutil&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;runtime&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;time&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">BitmapCovering&lt;/span>(&lt;span style="color:#a6e22e">zoom&lt;/span> &lt;span style="color:#66d9ef">uint8&lt;/span>, &lt;span style="color:#a6e22e">polygon&lt;/span> &lt;span style="color:#a6e22e">orb&lt;/span>.&lt;span style="color:#a6e22e">Polygon&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">roaring64&lt;/span>.&lt;span style="color:#a6e22e">Bitmap&lt;/span>, &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">roaring64&lt;/span>.&lt;span style="color:#a6e22e">Bitmap&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// compute the covering of the boundary linear ring
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">boundarySet&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">roaring64&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span>, &lt;span style="color:#a6e22e">ring&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">polygon&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">boundaryTiles&lt;/span>, &lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">tilecover&lt;/span>.&lt;span style="color:#a6e22e">Geometry&lt;/span>(&lt;span style="color:#a6e22e">orb&lt;/span>.&lt;span style="color:#a6e22e">LineString&lt;/span>(&lt;span style="color:#a6e22e">ring&lt;/span>), &lt;span style="color:#a6e22e">maptile&lt;/span>.&lt;span style="color:#a6e22e">Zoom&lt;/span>(&lt;span style="color:#a6e22e">zoom&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">tile&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">boundaryTiles&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">boundarySet&lt;/span>.&lt;span style="color:#a6e22e">Add&lt;/span>(&lt;span style="color:#a6e22e">pmtiles&lt;/span>.&lt;span style="color:#a6e22e">ZxyToID&lt;/span>(uint8(&lt;span style="color:#a6e22e">tile&lt;/span>.&lt;span style="color:#a6e22e">Z&lt;/span>), &lt;span style="color:#a6e22e">tile&lt;/span>.&lt;span style="color:#a6e22e">X&lt;/span>, &lt;span style="color:#a6e22e">tile&lt;/span>.&lt;span style="color:#a6e22e">Y&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Prepare the polygon in Web Mercator for point-in-polygon testing
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">polygonProjected&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">project&lt;/span>.&lt;span style="color:#a6e22e">Polygon&lt;/span>(&lt;span style="color:#a6e22e">polygon&lt;/span>.&lt;span style="color:#a6e22e">Clone&lt;/span>(), &lt;span style="color:#a6e22e">project&lt;/span>.&lt;span style="color:#a6e22e">WGS84&lt;/span>.&lt;span style="color:#a6e22e">ToMercator&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">interiorSet&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">roaring64&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">boundarySet&lt;/span>.&lt;span style="color:#a6e22e">Iterator&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span>.&lt;span style="color:#a6e22e">HasNext&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">id&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span>.&lt;span style="color:#a6e22e">Next&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// For every gap between tile IDs in the boundary, check if the next tile
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// is inside or outside using point-in-polygon
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> !&lt;span style="color:#a6e22e">boundarySet&lt;/span>.&lt;span style="color:#a6e22e">Contains&lt;/span>(&lt;span style="color:#a6e22e">id&lt;/span>&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span>.&lt;span style="color:#a6e22e">HasNext&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">z&lt;/span>, &lt;span style="color:#a6e22e">x&lt;/span>, &lt;span style="color:#a6e22e">y&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pmtiles&lt;/span>.&lt;span style="color:#a6e22e">IDToZxy&lt;/span>(&lt;span style="color:#a6e22e">id&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">tile&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">maptile&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">x&lt;/span>, &lt;span style="color:#a6e22e">y&lt;/span>, &lt;span style="color:#a6e22e">maptile&lt;/span>.&lt;span style="color:#a6e22e">Zoom&lt;/span>(&lt;span style="color:#a6e22e">z&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">planar&lt;/span>.&lt;span style="color:#a6e22e">PolygonContains&lt;/span>(&lt;span style="color:#a6e22e">polygonProjected&lt;/span>, &lt;span style="color:#a6e22e">project&lt;/span>.&lt;span style="color:#a6e22e">Point&lt;/span>(&lt;span style="color:#a6e22e">tile&lt;/span>.&lt;span style="color:#a6e22e">Center&lt;/span>(), &lt;span style="color:#a6e22e">project&lt;/span>.&lt;span style="color:#a6e22e">WGS84&lt;/span>.&lt;span style="color:#a6e22e">ToMercator&lt;/span>)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">interiorSet&lt;/span>.&lt;span style="color:#a6e22e">AddRange&lt;/span>(&lt;span style="color:#a6e22e">id&lt;/span>&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#a6e22e">i&lt;/span>.&lt;span style="color:#a6e22e">PeekNext&lt;/span>())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">boundarySet&lt;/span>, &lt;span style="color:#a6e22e">interiorSet&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">dat&lt;/span>, &lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">ioutil&lt;/span>.&lt;span style="color:#a6e22e">ReadFile&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;southamerica.geojson&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">geom&lt;/span>, &lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">geojson&lt;/span>.&lt;span style="color:#a6e22e">UnmarshalGeometry&lt;/span>(&lt;span style="color:#a6e22e">dat&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">polygon&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">geom&lt;/span>.&lt;span style="color:#a6e22e">Geometry&lt;/span>().(&lt;span style="color:#a6e22e">orb&lt;/span>.&lt;span style="color:#a6e22e">Polygon&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">m&lt;/span> &lt;span style="color:#a6e22e">runtime&lt;/span>.&lt;span style="color:#a6e22e">MemStats&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">zoom&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> uint8(&lt;span style="color:#ae81ff">15&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">start&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Now&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">boundary&lt;/span>, &lt;span style="color:#a6e22e">interior&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">BitmapCovering&lt;/span>(&lt;span style="color:#a6e22e">zoom&lt;/span>, &lt;span style="color:#a6e22e">polygon&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Println&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;covering size&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">boundary&lt;/span>.&lt;span style="color:#a6e22e">GetCardinality&lt;/span>()&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#a6e22e">interior&lt;/span>.&lt;span style="color:#a6e22e">GetCardinality&lt;/span>())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">covering&lt;/span>, &lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">tilecover&lt;/span>.&lt;span style="color:#a6e22e">Polygon&lt;/span>(&lt;span style="color:#a6e22e">polygon&lt;/span>, &lt;span style="color:#a6e22e">maptile&lt;/span>.&lt;span style="color:#a6e22e">Zoom&lt;/span>(&lt;span style="color:#a6e22e">zoom&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Println&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;covering size&amp;#34;&lt;/span>, len(&lt;span style="color:#a6e22e">covering&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">runtime&lt;/span>.&lt;span style="color:#a6e22e">ReadMemStats&lt;/span>(&lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">m&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Println&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;megabytes&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">m&lt;/span>.&lt;span style="color:#a6e22e">Alloc&lt;/span>&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">1024&lt;/span>&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">1024&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Println&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;took&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Since&lt;/span>(&lt;span style="color:#a6e22e">start&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Input polygon &lt;code>southamerica.geojson&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>{&amp;#34;type&amp;#34;:&amp;#34;Polygon&amp;#34;,&amp;#34;coordinates&amp;#34;:[[[-62.57541,14.915208],[-72.821509,14.915208],[-72.821509,14.915208],[-82.904973,6.291524],[-85.995066,-3.289539],[-73.959965,-23.278034],[-81.441244,-50.744789],[-72.821509,-57.406184],[-63.551229,-56.429744],[-58.184224,-42.141627],[-35.57775,-17.022015],[-31.186565,-5.235684],[-44.197485,2.400227],[-62.57541,14.915208]]]}
&lt;/code>&lt;/pre></description></item><item><title>What is a basemap, anyway?</title><link>https://protomaps.com/blog/what-is-a-basemap/</link><pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/what-is-a-basemap/</guid><description>&lt;p>The Protomaps project consists of:&lt;/p>
&lt;ul>
&lt;li>The open &lt;a href="https://docs.protomaps.com/pmtiles/">PMTiles&lt;/a> format;&lt;/li>
&lt;li>The &lt;a href="https://docs.protomaps.com/pmtiles/cli">tools and ecosystem&lt;/a> for creating and publishing PMTiles;&lt;/li>
&lt;li>A &lt;a href="https://docs.protomaps.com/basemaps/downloads">&lt;strong>basemap&lt;/strong> PMTiles&lt;/a> created from &lt;a href="https://www.openstreetmap.org">OpenStreetMap&lt;/a> data and other open sources.&lt;/li>
&lt;/ul>
&lt;p>Mapping apps need &lt;strong>background context&lt;/strong> to help humans understand &lt;em>where things are&lt;/em>. The base layer has water bodies, buildings and roads. You could use Google Maps and Apple maps for those, but Protomaps is open source!&lt;/p>
&lt;p>Two key aspects of a basemap are &lt;em>labeling&lt;/em> and &lt;em>generalization&lt;/em>. Text labels help users identify well-known places. Generalization is the choice of features that appear at zoomed-out overview levels. It&amp;rsquo;s what makes a basemap different from merely a &lt;a href="https://planet.openstreetmap.org">data dump of OpenStreetMap&lt;/a>. Some data necessary for generalization will never be part of OSM, such as importance rankings from &lt;a href="https://www.naturalearthdata.com">Natural Earth&lt;/a>.&lt;/p>
&lt;h2 id="basemap-origins">Basemap Origins&lt;/h2>
&lt;p>The Protomaps basemap is inspired by a long legacy of web mapping projects.&lt;/p>
&lt;p>Legendary San Francisco design studio &lt;a href="http://stamen.com">Stamen&lt;/a> created OSM basemaps as part of a &lt;a href="https://stamen.com/work/maps-stamen-com/">Knight News Challenge&lt;/a> grant. These basemaps were built with OpenStreetMap, PostGIS and Mapnik.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="stamen_toner.jpg" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The &lt;strong>Toner&lt;/strong> basemap was especially suited as a background to data visualization overlays, and was a popular style that strayed from the &amp;ldquo;road atlas&amp;rdquo; look of early web maps.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="stamen_watercolor.jpg" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The &lt;strong>Watercolor&lt;/strong> map took the raster slippy map rendering stack to its limit, and pushed the boundaries of what was recognizable as a basemap, while using familiar OpenStreetMap data.&lt;/p>
&lt;p>The original code for rendering Toner is available on GitHub at the &lt;a href="https://github.com/stamen/toner-carto">toner-carto&lt;/a> and &lt;a href="https://github.com/stamen/watercolor">watercolor&lt;/a> repositories.&lt;/p>
&lt;h2 id="mapzen">Mapzen&lt;/h2>
&lt;p>The Protomaps basemap is primarily derived from the Tilezen and Tangram projects that were developed at Mapzen from 2013 to 2018.&lt;/p>
&lt;p>The &lt;strong>Bubble Wrap&lt;/strong> style was a general-purpose basemap that included icons for points of interest.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="em-23-5.jpg" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;strong>Refill&lt;/strong> was not a single map, but a family of mono or duo-chrome maps in the vein of Toner that could be customized for different label densities:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="meiji-jingu-z17.jpg" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>You can read more about the cartography behind Tilezen on Mapzen&amp;rsquo;s former blog:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.mapzen.com/blog/bubble-wrap-carto/">Unboxing Bubble Wrap&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.mapzen.com/blog/refill-levels-of-detail-and-labels/">Refill - Levels of Detail&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="moving-web-maps-forward">Moving Web Maps Forward&lt;/h3>
&lt;p>One vital aspect of the Tilezen project is its open license - it&amp;rsquo;s available under permissive FOSS licenses, and is now a &lt;a href="https://www.linuxfoundation.org/press/press-release/mapzen-open-source-data-and-software-for-real-time-mapping-applications-to-become-a-linux-foundation-project">Linux Foundation project&lt;/a>. This provides a solid foundation to build a basemap, instead of having to start from scratch.&lt;/p>
&lt;p>Key parts of Tilezen that are re-used or replaced in Protomaps are:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>PMTiles is an alternative to the &lt;a href="https://github.com/tilezen/tapalcatl">Tapalcatl&lt;/a> storage system. A copy of the Tilezen dataset in PMTiles format is &lt;a href="https://pmtiles.io/?url=https%3A%2F%2Fr2-public.protomaps.com%2Fprotomaps-sample-datasets%2Ftilezen.pmtiles#map=0.51/0/0">available for viewing and download&lt;/a>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The Protomaps &lt;a href="https://docs.protomaps.com/basemaps/layers">Basemap Layers&lt;/a> Version 4 are backwards-compatible with &lt;a href="https://tilezen.readthedocs.io/en/latest/layers/">Tilezen Layers&lt;/a>, with the exception of a few helper tags.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The &lt;a href="https://github.com/tilezen/vector-datasource">Tilezen build pipeline&lt;/a> is replaced with a &lt;a href="https://github.com/protomaps/basemaps/tree/main/tiles">Planetiler profile&lt;/a> that runs in 2 hours on a fast machine, instead of days on AWS.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="protomaps-basemap">Protomaps Basemap&lt;/h3>
&lt;p>The Protomaps basemap also adopts &lt;a href="https://maplibre.org">MapLibre&lt;/a> as its frontend rendering library, instead of the Mapzen Tangram project, because of its features like map rotation and globe mode, and its ecosystem of plugins.&lt;/p>
&lt;p>For MapLibre we&amp;rsquo;ve had to design an entire new set of styles for the Tilezen tileset. The maps are designed in collaboration with &lt;a href="http://geraldinesarmiento.com">Geraldine Sarmiento&lt;/a> - you can check out her portfolio, including her work on the Stamen and Mapzen styles, at &lt;a href="http://geraldinesarmiento.com">her portfolio site&lt;/a>.&lt;/p>
&lt;p>Rather than needing to maintain and update many independent styles, the Protomaps basemap is &lt;strong>one parameterized style&lt;/strong> in TypeScript that can be customized using simple palette objects - think of it like a color scheme or paint job, applied to the same map &amp;ldquo;skeleton.&amp;rdquo; This lets different visual styles share common logic for the ordering and relative prominence of features.&lt;/p>
&lt;p>The maps include:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>A default light flavor&lt;/strong>. It includes icons originally developed for Mapzen&amp;rsquo;s Bubble Wrap style.&lt;/li>
&lt;/ul>
&lt;div style="text-align: center">
 &lt;img src="protomaps-images-light4.png" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;ul>
&lt;li>&lt;strong>A Dark mode&lt;/strong> variant of the light flavor.&lt;/li>
&lt;/ul>
&lt;div style="text-align: center">
 &lt;img src="protomaps-images-dark3.png" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;ul>
&lt;li>&lt;strong>Data visualization flavors&lt;/strong>, inspired by Refill. Designed for overlaying data or even &amp;ldquo;sandwiching&amp;rdquo; data.&lt;/li>
&lt;/ul>
&lt;div style="text-align: center">
 &lt;img src="protomaps-dataviz3-2.png" alt="" style="max-width: 100%;
 max-height: 600px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;h3 id="project-status">Project Status&lt;/h3>
&lt;p>The implementation of the above map styles is still a work in progress, but sufficient for a wide range of mapping use cases like data visualization! You can follow along at:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>the interactive &lt;a href="https://docs.protomaps.com/basemaps/layers">Basemaps Layers&lt;/a> documentation.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>the &lt;a href="https://github.com/protomaps/basemaps">basemaps&lt;/a> repo on GitHub, and the &lt;a href="https://github.com/protomaps/basemaps-assets">basemaps-assets&lt;/a> repo for fonts and sprites.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>This work is supported by an &lt;a href="https://protomaps.com/blog/nlnet-grant/">NLNet Foundation NGI0 grant.&lt;/a>&lt;/p></description></item><item><title>Slicing OpenStreetMap Just Got Fresher</title><link>https://protomaps.com/blog/slicing-openstreetmap-just-got-fresher/</link><pubDate>Fri, 29 Nov 2024 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/slicing-openstreetmap-just-got-fresher/</guid><description>&lt;p>Developing software for &lt;a href="https://openstreetmap.org">OpenStreetMap&lt;/a> data all starts with the &lt;a href="https://wiki.openstreetmap.org/wiki/PBF_Format">PBF&lt;/a> - it&amp;rsquo;s nodes, ways and relations in the raw, native OSM format. You can make tiled maps, PostGIS databases, or GeoJSON out of PBFs, but those conversions lose some tagging and metadata. With PBF, you get pure OpenStreetMap!&lt;/p>
&lt;p>The global OSM.org website provides a &lt;a href="https://planet.openstreetmap.org/">weekly copy&lt;/a> of the entire planet as one ~80GB PBF. And there&amp;rsquo;s convenient sites like &lt;a href="https://download.geofabrik.de/">Geofabrik Downloads&lt;/a> for getting just one country of data. These are amazing free resources, with one catch: if you&amp;rsquo;re contributing data to OSM, those sources are instantly out of date once you upload.&lt;/p>
&lt;h2 id="protomaps-extracts">Protomaps Extracts&lt;/h2>
&lt;div style="text-align: center">
 &lt;img src="old_and_new_ui.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;strong>Protomaps Extracts&lt;/strong> &lt;em>(previously hosted at protomaps.com/extracts)&lt;/em> aims to solve this - it&amp;rsquo;s a web portal to download minute-updated extracts of OSM for any area, up to 100 million nodes. Well, that&amp;rsquo;s how it used to be, because this is the last post about Protomaps Extracts here! The service has been migrated to an &lt;a href="https://openstreetmap.us/our-work/community-charter-projects/">OpenStreetMap US Community Project&lt;/a>, at the url &lt;a href="https://slice.openstreetmap.us">slice.openstreetmap.us&lt;/a>, and has:&lt;/p>
&lt;ul>
&lt;li>A flashy new logo and domain&lt;/li>
&lt;li>a new, 100% open source backend and UI, with no API keys or logins required&lt;/li>
&lt;li>a new GitHub organization &lt;a href="https://github.com/sliceosm/">SliceOSM&lt;/a> outside of the Protomaps umbrella project&lt;/li>
&lt;/ul>
&lt;h2 id="new-project-sliceosm">New project: SliceOSM&lt;/h2>
&lt;p>Think about OpenStreetMap as a big pizza party. You can&amp;rsquo;t eat a whole pizza yourself: you just want a a slice of your local city or country. And with minutely updates, if you add your own toppings, like pineapple or anchovies, your friends get those on their pizza slice too!&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="cover.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>By migrating into the &lt;a href="http://openstreetmap.us">OpenStreetMap US&lt;/a> nonprofit, this service:&lt;/p>
&lt;ul>
&lt;li>Brings the tool to a wider audience outside of just Protomaps map users&lt;/li>
&lt;li>Increases the open source maintenance &amp;ldquo;bus factor&amp;rdquo;&lt;/li>
&lt;li>In the future, can seamlessly integrate with other OSMUS initiatives such as &lt;a href="https://osmcha.org">OSMCha&lt;/a>, &lt;a href="http://opentrailmap.us">OpenTrailMap&lt;/a>, and &lt;a href="https://maproulette.org">MapRoulette&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Providing this free service is also supported by hosting credits from the &lt;a href="https://aws.amazon.com/opendata/open-data-sponsorship-program/">AWS Open Data program&lt;/a>.&lt;/p>
&lt;h2 id="project-structure">Project Structure&lt;/h2>
&lt;p>SliceOSM is built on the &lt;a href="https://github.com/bdon/OSMExpress">OSM Express&lt;/a> database for replicating the OpenStreetMap planet by the minute and spatial indexing via S2 cells. It&amp;rsquo;s intended to succeed Michal Migurski&amp;rsquo;s original &lt;a href="https://github.com/migurski/Extractotron">Metro Extracts&lt;/a> as well as Mapzen&amp;rsquo;s &lt;a href="https://www.mapzen.com/blog/metro-extracts-on-demand/">Extracts on Demand&lt;/a>.&lt;/p>
&lt;p>The project is designed to be modular and re-usable, with:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/SliceOSM/sliceosm-frontend">sliceosm-frontend&lt;/a> built on &lt;a href="http://terradraw.io">Terra Draw&lt;/a>, &lt;a href="http://americanamap.org">OSM Americana&lt;/a> and &lt;a href="http://maplibre.org">MapLibre GL&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/SliceOSM/sliceosm-api">sliceosm-api&lt;/a> for wrapping OSM Express calls in a go web server&lt;/li>
&lt;/ul>
&lt;h2 id="project-alternatives">Project Alternatives&lt;/h2>
&lt;p>SliceOSM is designed for city to country-sized slices, made by human editors. If you need larger areas, bulk slices, or automated use, some alternatives you should look into are:&lt;/p>
&lt;ul>
&lt;li>using &lt;a href="https://osmcode.org/osmium-tool/">osmium-tool&lt;/a> on a weekly planet file&lt;/li>
&lt;li>using &lt;a href="https://download.geofabrik.de">Geofabrik Downloads&lt;/a> for daily countries&lt;/li>
&lt;/ul></description></item><item><title>PMTiles on More Platforms</title><link>https://protomaps.com/blog/pmtiles-more-platforms/</link><pubDate>Mon, 28 Oct 2024 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/pmtiles-more-platforms/</guid><description>&lt;p>The &lt;a href="https://docs.protomaps.com/pmtiles/">PMTiles format&lt;/a> is a cloud-native way to visualize map data in the browser, directly from cloud storage. Browser decoding via the &lt;a href="https://pmtiles.io/typedoc/">TypeScript implementation&lt;/a> is ideal for most use cases, and accelerated deployment options run on &lt;a href="https://docs.protomaps.com/deploy/">AWS and Cloudflare&lt;/a>.&lt;/p>
&lt;p>Here&amp;rsquo;s some highlights of new platforms and integrations in 2024:&lt;/p>
&lt;h2 id="style-editor">Style Editor&lt;/h2>
&lt;div style="text-align: center">
 &lt;img src="maputnik.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;a href="https://github.com/maplibre/maputnik">Maputnik&lt;/a> is a visual editor for &lt;a href="https://maplibre.org/maplibre-style-spec/">MapLibre Style JSON&lt;/a> that&amp;rsquo;s been migrated to the &lt;a href="">MapLibre&lt;/a> GitHub umbrella organization. It&amp;rsquo;s the most full-featured way to tweak colors and label appearance. An experimental &lt;a href="https://github.com/bdon/maputnik">fork of Maputnik&lt;/a> adds PMTiles sources directly to the editor, so you can customize maps on cloud-hosted datasets without any server or proxy setup!&lt;/p>
&lt;p>Try it at &lt;a href="https://editor.protomaps.com">editor.protomaps.com&lt;/a>.&lt;/p>
&lt;h2 id="openlayers">OpenLayers&lt;/h2>
&lt;div style="text-align: center">
 &lt;img src="openlayers.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The &lt;a href="https://www.npmjs.com/package/ol-pmtiles">ol-pmtiles library&lt;/a> has reached major version 1.0. New features include TypeScript support and custom non-HTTP data sources, such as local files. This library can be bundled with your OpenLayers application to display both raster or vector tilesets using the built-in tile display classes.&lt;/p>
&lt;p>See &lt;a href="https://pmtiles.io/examples/openlayers/vector.html">example page&lt;/a> and &lt;a href="https://github.com/protomaps/PMTiles/blob/main/openlayers/examples/vector.html">code&lt;/a> for OpenLayers vector tile integration.&lt;/p>
&lt;h2 id="aws-cloudformation">AWS CloudFormation&lt;/h2>
&lt;p>PMtiles is designed to run on &lt;a href="https://docs.protomaps.com/pmtiles/cloud-storage">plain S3-like storage&lt;/a>, and has an identical developer experience for small neighborhood maps, scaling up to 100GB+ planet archives. For websites with lots of traffic, it&amp;rsquo;s best to use PMTiels in conjunction with a CDN integration, like with AWS Lambda and Cloudfront, to &lt;a href="https://docs.protomaps.com/deploy/">serve edge-cached ZXY tiles&lt;/a> to viewers, eliminating the overhead of client-side decoding.&lt;/p>
&lt;p>Previously the method to accomplish this on AWS was to click in the console for IAM role setup, lambda function deployment and CloudFront configuration. This was too time-consuming and error-prone.&lt;/p>
&lt;p>The automation options we considered were Terraform, CDK and &lt;a href="https://aws.amazon.com/cloudformation/">CloudFormation&lt;/a>. CloudFormation ended up being the simplest choice, since anyone access to the AWS console can &lt;a href="https://docs.protomaps.com/deploy/aws#_2-cloudformation-template">one-click upload a single YAML&lt;/a> and go straight from a private S3 bucket to CDN tiles - no CLI required!&lt;/p>
&lt;p>Thanks to contributors &lt;a href="https://github.com/Honricris">Carlos Honrado&lt;/a> for kicking this off and &lt;a href="https://github.com/charliemcgrady">Charlie McGrady&lt;/a> for reviewing this feature.&lt;/p>
&lt;h2 id="google-cloud-and-azure">Google Cloud and Azure&lt;/h2>
&lt;p>Along with AWS and CLoudflare, two new platforms have support for serverless decoding: Google Cloud and Microsoft Azure. This is a perfect fit for projects already built on GCP or Azure and leveraging blob storage.&lt;/p>
&lt;p>Both of these deployment methods use the &lt;a href="https://hub.docker.com/r/protomaps/go-pmtiles/tags">go-pmtiles&lt;/a> Docker image to run on a scale-to-zero container platform, either Cloud Run or Azure Container Apps. Most importantly, the authentication with private storage is built-in and doesn&amp;rsquo;t require manually managing credentials.&lt;/p>
&lt;p>Check out the guides for accelerating PMTiles on &lt;a href="https://docs.protomaps.com/deploy/google-cloud">Google Cloud&lt;/a> and &lt;a href="https://docs.protomaps.com/deploy/azure">Microsoft Azure&lt;/a>.&lt;/p>
&lt;h2 id="server">Server&lt;/h2>
&lt;p>There&amp;rsquo;s plenty of reasons not to use any of the major cloud providers - you might simply want to run a world map on a server in your basement, on an airplane, or on an affordable VPS.&lt;/p>
&lt;p>The docs for running the &lt;a href="https://docs.protomaps.com/pmtiles/cli">pmtiles CLI&lt;/a> have been revamped, with details on &lt;code>pmtiles serve&lt;/code> and setting up CORS and remote or local archives. Serving tiles this way is great for development too; for production use it&amp;rsquo;s best behind a reverse proxy like Nginx or Caddy.&lt;/p>
&lt;h3 id="caddy">Caddy&lt;/h3>
&lt;p>Additionally there&amp;rsquo;s now a first-class &lt;a href="https://caddyserver.com">Caddy&lt;/a> plugin for PMTiles, so you don&amp;rsquo;t need to keep any separate process running, the PMTiles decoding is embedded directly into the webserver! Configuration is directly in the Caddyfile:&lt;/p>
&lt;pre tabindex="0">&lt;code> handle_path /tiles/* {
 pmtiles_proxy {
 bucket https://mybucket.com
 cache_size 256
 public_url https://example.com/tiles
 }
 }
&lt;/code>&lt;/pre>&lt;p>See &lt;a href="https://docs.protomaps.com/deploy/server#installation">the docs&lt;/a> on downloading a build with the plugin.&lt;/p>
&lt;p>Thanks to &lt;a href="http://nlnet.nl">NLnet&lt;/a> and &lt;a href="https://www.radicallyopensecurity.com">Radically Open Security&lt;/a> for funding a security audit of the go-pmtiles server implementation as part of the &lt;a href="https://nlnet.nl/core/">NGI Zero Core&lt;/a> grant program.&lt;/p></description></item><item><title>You Might Not Want PMTiles</title><link>https://protomaps.com/blog/you-might-not-want-pmtiles/</link><pubDate>Wed, 22 May 2024 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/you-might-not-want-pmtiles/</guid><description>&lt;p>&lt;a href="https://docs.protomaps.com/pmtiles/">PMTiles&lt;/a> is the open archive format that underlies the Protomaps ecosystem. It&amp;rsquo;s a single file that enables deploying an &lt;a href="https://protomaps.com/blog/blog/open-core-to-open-source/">entire world map&lt;/a> as a static artifact, making mapping accessible to the widest audience of front-end developers and &lt;a href="https://thegeomob.com/podcast/episode-94">neogeographers&lt;/a>.&lt;/p>
&lt;p>However, PMTiles isn&amp;rsquo;t the right solution for all mapping applications.&lt;/p>
&lt;p>PMTiles is made for &lt;em>web-based viewing of large, mostly static datasets.&lt;/em>, for example:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://docs.protomaps.com/basemaps/downloads">base maps&lt;/a> built from &lt;a href="https://openstreetmap.org">OpenStreetMap&lt;/a> data.&lt;/li>
&lt;li>An &lt;a href="https://hanbyul-here.github.io/seoul-building-explorer-2024">interactive exploration of building ages in Seoul&lt;/a> by &lt;a href="https://x.com/boonpeel">Hanbyul Jo&lt;/a>.&lt;/li>
&lt;li>A &lt;a href="https://apps.npr.org/plant-hardiness-garden-map/">climate change story map&lt;/a> of USDA gardening zones by &lt;a href="https://blog.apps.npr.org">NPR&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>All these have a few things in common:&lt;/p>
&lt;ul>
&lt;li>The experience is built on the &lt;strong>web platform&lt;/strong>, instead of a local desktop application.&lt;/li>
&lt;li>The total information to be explored is &lt;strong>over a few megabytes&lt;/strong> - more than can be loaded at once for a pleasant website experience.&lt;/li>
&lt;li>The dataset changes &lt;strong>at most daily&lt;/strong>, or never.&lt;/li>
&lt;/ul>
&lt;p>If your application doesn&amp;rsquo;t have these three properties, there are simpler alternatives to PMTiles.&lt;/p>
&lt;h2 id="geojson">GeoJSON&lt;/h2>
&lt;p>If you&amp;rsquo;re building a web-based map of static information, but your data is &lt;em>small&lt;/em>, you should serve it as &lt;strong>a single GeoJSON file.&lt;/strong>&lt;/p>
&lt;p>With MapLibre it&amp;rsquo;s as easy as &lt;a href="https://maplibre.org/maplibre-style-spec/sources/#geojson">adding a GeoJSON source&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">sources&lt;/span>&lt;span style="color:#f92672">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;geojson-marker&amp;#34;&lt;/span>&lt;span style="color:#f92672">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;type&amp;#34;&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#e6db74">&amp;#34;geojson&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;data&amp;#34;&lt;/span>&lt;span style="color:#f92672">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;type&amp;#34;&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Feature&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;geometry&amp;#34;&lt;/span>&lt;span style="color:#f92672">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;type&amp;#34;&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Point&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;coordinates&amp;#34;&lt;/span>&lt;span style="color:#f92672">:&lt;/span> [&lt;span style="color:#ae81ff">12.550343&lt;/span>, &lt;span style="color:#ae81ff">55.665957&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;properties&amp;#34;&lt;/span>&lt;span style="color:#f92672">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This saves you the hassle of converting your data into tiles, and you can use all the same map styling and interaction techniques as tiled data.&lt;/p>
&lt;h2 id="postgis">PostGIS&lt;/h2>
&lt;p>If you&amp;rsquo;re building a web-based map for a sizeable dataset, but your dataset is &lt;em>dynamic&lt;/em>, with frequent user updates, you should &lt;strong>store your features in a transactional database.&lt;/strong>&lt;/p>
&lt;p>It&amp;rsquo;s possible to update a PMTiles file regularly, but it requires re-uploading the file each time on storage. This is fine for hourly or daily updates: any higher frequency uses an uneconomical amount of data transfer.&lt;/p>
&lt;p>&lt;a href="https://postgis.net">PostGIS&lt;/a> is the industry standard transactional database for geographical features. Popular ways to fetch tiled data from PostGIS are &lt;a href="https://github.com/CrunchyData/pg_tileserv">pg_tileserv&lt;/a>, &lt;a href="https://github.com/maplibre/martin">martin&lt;/a> and the raw &lt;a href="https://postgis.net/docs/ST_AsMVT.html">ST_asMVT&lt;/a> function.&lt;/p>
&lt;p>A major challenge for web maps, including PostGIS-based ones, is how to generalize data for lower-zoom tiles. One method is to &lt;a href="https://github.com/bdon/supabase-vector-tile/blob/main/function.sql#L16">selectively drop attribute data at zoom levels&lt;/a> to make lower zoom overviews lighter.&lt;/p>
&lt;h3 id="postgis-on-supabase">PostGIS on Supabase&lt;/h3>
&lt;p>If PostGIS is the right fit, but standing up a database looks intimidating, take a look at &lt;a href="http://supabase.com">Supabase&lt;/a>, a hosted PostgreSQL platform with authentication and client APIs built in.&lt;/p>
&lt;p>I&amp;rsquo;ve developed a demo application using PL/pgSQL functions that call the &lt;code>ST_asMVT&lt;/code> function. This function is called directly from the browser and passed into MapLibre using &lt;code>addProtocol&lt;/code>. Browse the the &lt;a href="http://overturemaps.org">Overture Places&lt;/a> dataset for Singapore, fetched straight from the PostGIS database. The example code is on GitHub at &lt;a href="https://github.com/bdon/supabase-vector-tile">supabase-vector-tile&lt;/a>.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="cover.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;a href="https://bdon.github.io/supabase-vector-tile/">Overture Places in Singapore - live PostGIS tiles from Supabase&lt;/a>&lt;/p>
&lt;h2 id="geoparquet">GeoParquet&lt;/h2>
&lt;p>If you&amp;rsquo;re exploring large, static datasets, but don&amp;rsquo;t need publishing on the web, you can forgo tiles and directly visualize files using desktop software.&lt;/p>
&lt;p>Tiling with tools like &lt;a href="https://github.com/felt/tippecanoe">tippecanoe&lt;/a> requires computing generalized overview tiles in advance, and is designed for fetching small, optimized pieces of data over the Internet. If the network is not the bottleneck, and you have your dataset locally, QGIS is an excellent, open-source solution for visualization and mapmaking.&lt;/p>
&lt;p>Along with GeoJSON and &lt;a href="http://flatgeobuf.org">FlatGeobuf&lt;/a>, &lt;a href="https://geoparquet.org">GeoParquet&lt;/a> is a new format that can store large datasets efficiently and is interoperable with open source data tools. GeoParquet 1.0.0 is supported in &lt;a href="https://gdal.org/drivers/vector/parquet.html">GDAL version 3.8.0 and above&lt;/a>.&lt;/p>
&lt;h3 id="lonboard">Lonboard&lt;/h3>
&lt;p>Tools like &lt;a href="https://developmentseed.org/lonboard/latest/">Lonboard&lt;/a> enable visualization of GeoParquet in Jupyter notebooks. It&amp;rsquo;s possible to publish these on the web using hosted notebooks, though transferring tens or hundreds of megabytes leads to more waiting than a tiled map experience. For local data, though, GeoParquet + Lonboard is a great solution for exploratory data analysis that saves you the trouble of converting to a network-optimized, tiled format.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="path-layer-roads.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;a href="https://developmentseed.org/lonboard/latest/examples/">Lonboard Examples - Development Seed Documentation&lt;/a>&lt;/p></description></item><item><title>Protomaps receives NLnet open source grant</title><link>https://protomaps.com/blog/nlnet-grant/</link><pubDate>Wed, 21 Feb 2024 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/nlnet-grant/</guid><description>&lt;div style="text-align: center">
 &lt;img src="cover.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The Protomaps open source project is a recipient of a &lt;a href="http://nlnet.nl">NLnet open source grant!&lt;/a> This grant is part of the &lt;a href="http://ngi.eu">NGI (Next Generation Internet)&lt;/a> initiative of the European Commission.&lt;/p>
&lt;h2 id="motivation">Motivation&lt;/h2>
&lt;p>The goals of the &lt;a href="https://nlnet.nl/core/">NGI Zero Core initiative&lt;/a> are to develop &lt;em>alternatives and improvements to core internet hardware, software and protocols&lt;/em>, in order to remove &lt;em>gatekeepers, choke points and surveillance capabilities&lt;/em> on the web.&lt;/p>
&lt;p>Maps are everywhere on the Internet. Think of any popular retail site, travel search or local government portal. The infrastructure of the web, from languages like JavaScript, front-end frameworks, and databases, is overwhelmingly free software. &lt;strong>Maps are one of the few remaining choke points of an otherwise open web.&lt;/strong> Developers are locked-in to proprietary vendors like Google Maps; worse, all web traffic is leaked to third party APIs, creating problems for privacy and &lt;a href="https://nlnet.nl/project/GDPRcompliance/">GDPR compliance.&lt;/a>&lt;/p>
&lt;p>The Protomaps project isn&amp;rsquo;t a hypothetical idea - you can use it &lt;a href="https://docs.protomaps.com/guide/getting-started">right now&lt;/a> to deploy a world map as a single file, with none of the typical complications of self-hosted networked software.&lt;/p>
&lt;p>Our NLnet grant will focus on enhancing key parts of Protomaps to advance maps on the open web, resulting in software distributed under &lt;a href="https://protomaps.com/blog/blog/open-core-to-open-source/">a standard FOSS license&lt;/a>, to be built upon and reused freely.&lt;/p>
&lt;h2 id="maps-for-the-public-sector">Maps for the Public Sector&lt;/h2>
&lt;p>The focus of our initial grant is &lt;em>enhancing the viability of Protomaps for public sector use.&lt;/em> This aligns with European mandates for open source software in government, and hopefully helps erode &lt;a href="https://joemorrison.medium.com/why-hasnt-open-source-software-disrupted-esri-a55896dd6f58">de facto monopolies on public sector GIS by large corporations.&lt;/a>&lt;/p>
&lt;p>The general-purpose &lt;a href="https://docs.protomaps.com/basemaps/downloads">&amp;ldquo;base layer&amp;rdquo; component&lt;/a> of Protomaps is downstream of &lt;a href="http://openstreetmap.org">OpenStreetMap&lt;/a>. This grant will make OSM-based apps simple to deploy for local governments, including key data layers like points of interest. The current &lt;a href="https://docs.protomaps.com/basemaps/downloads#current-version">daily build channel&lt;/a> lets public improvements to OSM appear on tilesets within 24 hours.&lt;/p>
&lt;h2 id="localization">Localization&lt;/h2>
&lt;p>A key requirement of public sector use is &lt;em>localization&lt;/em>: displaying maps in the local language, or even multiple written languages. Multilingual audiences, especially in countries with many official languages like Belgium, are underserved by technology companies that focus on commercial use in North America and the English-speaking world. Notably, The &lt;a href="http://maplibre.org">MapLibre GL map rendering library&lt;/a>, an open source fork of Mapbox GL JS, does not support writing systems of South and Southeast Asia. This presents a &lt;strong>barrier to open source adoption&lt;/strong> for users in countries like Nepal and India.&lt;/p>
&lt;p>Project contributor Oliver Wipfli has created a &lt;a href="https://github.com/wipfli/positioned-glyph-font">proof-of-concept Nepali map on GitHub&lt;/a>:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="nepali.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>You can see Brandon&amp;rsquo;s presentations at &lt;a href="https://www.youtube.com/watch?v=rx-ku7O3Xjc">State of the Map 2021&lt;/a> and &lt;a href="https://www.youtube.com/watch?v=ZCO3-DqqJXM&amp;amp;t=19067s">FOSS4G Asia 2023&lt;/a> covering localization for map rendering.&lt;/p>
&lt;h2 id="security--privacy">Security &amp;amp; Privacy&lt;/h2>
&lt;p>The basic Protomaps deployment pattern is static files, taking advantage of HTTP Range Requests. Since this design requires no specialized server software, it can rely on mature, secure HTTP servers like nginx or &lt;a href="http://caddyserver.com">Caddy&lt;/a>. &lt;a href="https://docs.protomaps.com/pmtiles/cloud-storage">Cloud storage&lt;/a> deployment options can satisfy requirements for placing data in specific jurisdictions.&lt;/p>
&lt;p>As part of the &lt;a href="https://nlnet.nl/NGI0/services/">NLnet grant program&lt;/a>, the Protomaps open source system is undergoing a security audit by a third party firm. This will cover the server and command line tools, as well as documented best practices for public sector deployments.&lt;/p>
&lt;h2 id="collaborators">Collaborators&lt;/h2>
&lt;p>The best part of the NLnet grant program: it&amp;rsquo;s open to &lt;a href="https://nlnet.nl/core/eligibility/index.html">individuals or companies anywhere in the world&lt;/a>, not just those in the EU! Our small team of independent developers for this grant:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://github.com/bdon">Brandon Liu&lt;/a> - lead developer of Protomaps project, based in Taipei, Taiwan - maintainer of &lt;a href="http://github.com/protomaps/pmtiles">PMTiles&lt;/a> open format and ecosystem.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://github.com/wipfli">Oliver Wipfli&lt;/a> - contributor to &lt;a href="https://maplibre.org">MapLibre&lt;/a> project, based in Zürich, Switzerland; researching WebGL-accelerated multilingual text for maps.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="read-more">Read More&lt;/h2>
&lt;p>You can view a list of hundreds of NLnet-funded projects &lt;a href="https://nlnet.nl/project/index.html">on their website.&lt;/a>&lt;/p></description></item><item><title>Transitioning Protomaps from Open Core to Open Source</title><link>https://protomaps.com/blog/open-core-to-open-source/</link><pubDate>Fri, 16 Feb 2024 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/open-core-to-open-source/</guid><description>&lt;div style="text-align: center">
 &lt;img src="cover.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The &lt;a href="https://github.com/protomaps">Protomaps Project&lt;/a> has gained several new repositories in the past year, including the serverless implementations for AWS and Cloudflare, and a &lt;a href="https://github.com/protomaps/basemaps">new basemaps repository&lt;/a>, which generates a full planet &lt;a href="https://docs.protomaps.com/pmtiles/">PMTiles archive&lt;/a> from &lt;a href="https://openstreetmap.org">OpenStreetMap&lt;/a>. A free daily build of the planet output is available on &lt;a href="https://maps.protomaps.com/builds">maps.protomaps.com/builds&lt;/a>.&lt;/p>
&lt;p>These additional components complete the transition from an &lt;em>open core&lt;/em> project into an &lt;strong>open source&lt;/strong> one, distributed under a standard BSD License.&lt;/p>
&lt;h2 id="what-is-open-core">What is Open Core?&lt;/h2>
&lt;p>There&amp;rsquo;s a paradox at the heart of open source development:&lt;/p>
&lt;ul>
&lt;li>The best FOSS projects have maintainers working on them full-time as their job.&lt;/li>
&lt;li>Giving away software for free is a very bad way to make a living.&lt;/li>
&lt;/ul>
&lt;p>At one extreme, FOSS developers are &lt;strong>employed directly by large corporations with an unrelated business model.&lt;/strong> The open source project is a necessary, but not sufficient, part of the company&amp;rsquo;s overall product. That&amp;rsquo;s why Vercel develops &lt;a href="https://nextjs.org">Next.JS&lt;/a>, and Facebook funds development of &lt;a href="http://facebook.github.io/zstd/">ZStandard&lt;/a>. These are success stories where developers are paid to work on FOSS. But continued investment in the project is at the whims of the patron companies. It&amp;rsquo;s a fit for mature infrastructure projects, but not early-stage ones that attempt to change some status quo.&lt;/p>
&lt;p>At the other extreme are early-stage companies and bootstrapped developers. A strategy for funding FOSS development, especially in the developer tools space, is &lt;em>open core&lt;/em> - use a fully open source project to attract adoption, which funnels into sales of a premium, proprietary product.&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tailwindui.com">Tailwind UI&lt;/a> is a proprietary bundle of templates by the creators of Tailwind CSS.&lt;/li>
&lt;li>&lt;a href="https://sidekiq.org">Sidekiq Pro&lt;/a> is a a flat-fee, enhanced version of the Sidekiq Ruby job queue.&lt;/li>
&lt;/ul>
&lt;h3 id="foss-vs-saas">FOSS vs. SaaS&lt;/h3>
&lt;p>Another strategy is to run a paid, hosted version of a fully open source project. This is the approach used by &lt;a href="https://zulip.com">Zulip&lt;/a>, &lt;a href="https://supabase.com">Supabase&lt;/a> and &lt;a href="https://tailscale.com">Tailscale&lt;/a>. &lt;strong>SaaS is, bar none, the best business model for monetizing software,&lt;/strong> for a few key reasons:&lt;/p>
&lt;ul>
&lt;li>&lt;em>Adoption&lt;/em>: Onboarding is instant with no installation, and software can be updated behind-the-scenes.&lt;/li>
&lt;li>&lt;em>Digital Rights Management (DRM)&lt;/em>: Access can be terminated if users don&amp;rsquo;t pay.&lt;/li>
&lt;li>&lt;em>Price Discrimination&lt;/em>: Products are segmented into tiers so companies with higher willingness to pay, pay more.&lt;/li>
&lt;/ul>
&lt;p>As I described in &lt;a href="https://protomaps.com/blog/blog/free-tier-maps/">Rethinking the Free Tier for Maps&lt;/a>, SaaS as a path to financial sustainablity is not a good fit for the Protomaps project, because unlike a database-backed web service, the fundamental design of Protomaps makes it as &lt;em>easy to deploy as possible&lt;/em> - a single file on cloud storage. Acquiring the high-ticket customers necessary to build a SaaS business demands &lt;strong>convincing businesses that operating Protomaps is difficult&lt;/strong>. It simply isn&amp;rsquo;t, and I &lt;a href="https://docs.protomaps.com/deploy/">give you all the tools to do it in 20 minutes.&lt;/a>&lt;/p>
&lt;h2 id="the-open-core-experiment">The Open Core Experiment&lt;/h2>
&lt;p>Protomaps is not &lt;a href="https://stephango.com/vcware">&amp;ldquo;VCware&amp;rdquo;&lt;/a>. It&amp;rsquo;s the result of 4 years of bootstrapped, full-time independent developer work, and ensuring its continued development for another 4 years and more cannot be achieved by burning investor money. It must experiment with a balance between user-supported commercialization and its open source ecosystem.&lt;/p>
&lt;p>The core of Protomaps is the PMTiles format and ecosystem, which is an open spec in the public domain. Dozens of map apps use the format to serve their own datasets, like &lt;a href="https://www.kschaul.com/post/2023/02/16/how-the-post-is-replacing-mapbox-with-open-source-solutions/">the Washington Post&lt;/a> and the &lt;a href="https://www.zoningatlas.org/atlas">National Zoning Atlas&lt;/a>.&lt;/p>
&lt;p>The &lt;em>basemap&lt;/em>, or background underlay map, is a necessary complement to your own data. Popular solutions for basemaps are subscription APIs from Google and Esri. The first Protomaps commercial product offering was a one-time basemap download - not an API, not a subscription, just a download with a &lt;strong>big Buy Now button&lt;/strong>:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="storefront.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>This enables companies to deploy a self-contained mapping solution for one flat fee, on the cloud infrastructure they already use. The total costs end up &lt;a href="https://docs.protomaps.com/deploy/cost">10 to 100x less&lt;/a> than a hosted solution like Google Maps, and it works forever.&lt;/p>
&lt;h2 id="leaving-open-core">Leaving Open Core&lt;/h2>
&lt;p>The Protomaps Store is closed now, and everything previously proprietary is &lt;a href="https://github.com/protomaps">open source on GitHub&lt;/a>. It had some potential for sustaining the development of the open source project, but suffers key drawbacks of open core:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>It splits development effort between proprietary parts and open source parts. For a single full-time developer, this draws attention away from the the higher-impact FOSS project.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The proprietary nature and &amp;ldquo;bus factor&amp;rdquo; of being an independent bootstrapped project presents a risk to potential commercial adopters.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>It alienates users. Maintainers waste time explaining which parts are open and which are not, and contributions that overlap with the proprietary components are excluded.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>It creates a licensing mess, requiring specialized terms on how data artifacts can be stored and distributed to re-introduce scarcity.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="sponsor-supported-saas">Sponsor-supported SaaS&lt;/h3>
&lt;p>Offering a hosted SaaS is crucial for web apps that don&amp;rsquo;t need the flexibility of controlling the data on their own storage. The Protomaps API now accepts payments via &lt;a href="http://github.com/sponsors/protomaps">GitHub Sponsors&lt;/a>. This &lt;strong>centers the open source code over the inherently proprietary API&lt;/strong>: at any point, you are free to exit the API and run Protomaps yourself, and your monetary contribution continues to sustain the project.&lt;/p>
&lt;p>&lt;em>You can also cache any results from the tiles. Please don&amp;rsquo;t heavily scrape the API though, that&amp;rsquo;s inefficient; you can download the whole file for free at &lt;a href="https://maps.protomaps.com/builds">maps.protomaps.com/builds&lt;/a>, or bulk extract a region using the pmtiles CLI.&lt;/em>&lt;/p>
&lt;h3 id="new-commercial-distribution">New Commercial Distribution&lt;/h3>
&lt;p>&lt;a href="https://maps.protomaps.com/builds">The daily build&lt;/a> of the Protomaps basemap is created from up-to-date OpenStreetMap data, and distributed as an &lt;a href="https://osmfoundation.org/wiki/Licence/Community_Guidelines/Produced_Work_-_Guideline">ODbL Produced Work&lt;/a>. This ensures that map viewers can contribute data into OSM and have free access to an updated map within 24 hours.&lt;/p>
&lt;p>&lt;del>Alongside this free build is a new &lt;em>Protomaps Commercial Distribution&lt;/em> built from the &lt;a href="http://daylightmap.org">Daylight Map&lt;/a> subset of OSM, which can be a better fit for public-facing deployments by companies. Access to this is granted via the second tier of &lt;a href="https://github.com/sponsors/protomaps">GitHub Sponsors&lt;/a>.&lt;/del> &lt;em>Update: The Commercial Distribution is currently paused until later in 2024!&lt;/em>&lt;/p>
&lt;p>You can buy it once, copy it to your own cloud storage, and never pay for it again. A monthly sponsorship gives you access to new versions.&lt;/p>
&lt;p>Any previous customer of the Protomaps Store has complimentary access to the Commercial Distribution for one year.&lt;/p>
&lt;h3 id="protomaps-access">Protomaps Access&lt;/h3>
&lt;p>The third product tier available through &lt;a href="http://github.com/sponsors/protomaps">GitHub Sponsors&lt;/a> is &lt;strong>Access&lt;/strong> - direct communications with the project lead developer, via your company&amp;rsquo;s Slack or Teams. This includes:&lt;/p>
&lt;ul>
&lt;li>Evaluation of private datasets and deployment infrastructure&lt;/li>
&lt;li>Recommendations on geospatial architecture, even if it&amp;rsquo;s not Protomaps: PostGIS, OpenLayers and more&lt;/li>
&lt;li>Ensures knowledge is accessible to your entire team&lt;/li>
&lt;/ul>
&lt;p>There&amp;rsquo;s no long-term commitment for Access - a single month can work as a training course for your org on how to simplify your geospatial apps. Many companies have already adopted Protomaps into critical parts of their product, and Access is the best way of ensuring continued investment in that infrastructure.&lt;/p>
&lt;h2 id="see-also">See Also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>This blog post summarizes my FOSS4G NA talk, which is &lt;a href="https://www.youtube.com/watch?v=c-oeMR2aUXE">available online here.&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://redmonk.com/sogrady/2019/03/15/cloud-open-source-powder-keg/">The Cloud and Open Source Powder Keg on RedMonk&lt;/a>&lt;/p>
&lt;/li>
&lt;/ul></description></item><item><title>What is a clustered PMTiles archive?</title><link>https://protomaps.com/blog/pmtiles-cluster/</link><pubDate>Wed, 17 Jan 2024 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/pmtiles-cluster/</guid><description>&lt;div style="text-align: center">
 &lt;img src="cover.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>One detail in the PMTiles &lt;a href="https://github.com/protomaps/PMTiles/blob/main/spec/v3/spec.md">version 3 specification&lt;/a> is a boolean flag called &lt;em>clustered&lt;/em>. Popular tools like &lt;a href="https://docs.protomaps.com/pmtiles/cli">the pmtiles CLI&lt;/a>, &lt;a href="https://github.com/felt/tippecanoe">tippecanoe&lt;/a> and &lt;a href="https://github.com/onthegomap/planetiler">Planetiler&lt;/a> always create clustered archives. PMTiles is an open specification in the public domain, so this post is to aid developers in implementing this optional feature.&lt;/p>
&lt;h2 id="clustered">Clustered&lt;/h2>
&lt;p>The term &lt;em>clustered&lt;/em> comes from &lt;a href="https://en.wikipedia.org/wiki/Relational_database">relational databases&lt;/a>: the &lt;a href="https://www.postgresql.org/docs/current/sql-cluster.html">PostgreSQL manual&lt;/a> defines that &lt;em>&amp;ldquo;When a table is clustered, it is physically reordered based on the index information.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>A PostgreSQL index may be created over any column or set of columns; the directories of a PMTiles archive are the analogue of a database index, where the lookup key is the &lt;a href="https://protomaps.com/blog/pmtiles-v3-hilbert-tile-ids/">Hilbert TileID&lt;/a>. Setting the &lt;code>clustered&lt;/code> property of a PMTiles archive means that &lt;em>the ordering of the tile data on disk matches the directories.&lt;/em> Since directories must be sorted by Hilbert TileID, the tiles must also be in Hilbert order.&lt;/p>
&lt;h2 id="de-duplication">De-duplication&lt;/h2>
&lt;p>Ordering the tile contents in PMTiles has one small catch - tiles are allowed to be &lt;strong>de-duplicated&lt;/strong>. This is essential for representing the ocean on basemaps, otherwise &lt;a href="https://protomaps.com/blog/dynamic-maps-static-storage/">70% of the archive would be repetitive water tiles.&lt;/a>&lt;/p>
&lt;p>A toy example of a tileset with 4 tiles of &lt;code>length=100&lt;/code>, 2 of which are the same tile contents:&lt;/p>
&lt;pre tabindex="0">&lt;code> ┌───┐┌───┐
TileID=1, offset=0, │ A ││ O │
TileID=2, offset=200 (ocean tile) └───┘└───┘
TileID=3, offset=100 ┌───┐┌───┐
TileID=4, offset=200 (ocean tile) │ O ││ B │ 
 └───┘└───┘
&lt;/code>&lt;/pre>&lt;p>Because the tile with &lt;code>offset=200&lt;/code> appears twice, it&amp;rsquo;s ambiguous exactly where it should go relative to other tiles. To resolve this, the PMTiles spec defines &lt;em>clustered&lt;/em> as only having &lt;strong>backward references&lt;/strong> to lower offsets in the file, never jumping forward to a higher offset.&lt;/p>
&lt;pre tabindex="0">&lt;code> ┌───┐┌───┐
TileID=1, offset=0, │ A ││ O │
TileID=2, offset=100 (ocean tile) └───┘└───┘
TileID=3, offset=200 ┌───┐┌───┐
TileID=4, offset=100 (ocean tile) │ O ││ B │ 
 └───┘└───┘
&lt;/code>&lt;/pre>&lt;p>A properly clustered archive has the first tile contents at offset 0, and all unique tile contents are contiguous for the entire file.&lt;/p>
&lt;h2 id="locality">Locality&lt;/h2>
&lt;p>The historical motivation for clustering database tables was to improve &lt;a href="https://en.wikipedia.org/wiki/Locality_of_reference">locality&lt;/a>. When most servers used magnetic hard drives, disk access to nearby parts of a file was much faster than far-away parts. PMTiles is designed to work with &lt;a href="https://docs.protomaps.com/pmtiles/cloud-storage">storage systems like S3&lt;/a>: cloud blob store APIs don&amp;rsquo;t exhibit a huge difference in latency depending on data locality. But if you&amp;rsquo;re accessing a &lt;code>.pmtiles&lt;/code> on disk, or on a magnetic hard drive, the seek time should be lower for a typical task like requesting multiple tiles in one area.&lt;/p>
&lt;h2 id="directory-delta-encoding">Directory delta encoding&lt;/h2>
&lt;p>A planet-scale tileset like the Protomaps basemap addresses over one billion tiles. Storing this directory using a naive encoding like &lt;code>TileID(uint64)&lt;/code>,&lt;code>Offset(uint64)&lt;/code>,&lt;code>Length(uint64)&lt;/code> is about 20 gigabytes of raw data. Since the map user can zoom and pan anywhere on the map, and parts of the directory must be fetched on-demand, it&amp;rsquo;s key to make this encoding as small as possible. The &lt;a href="https://protomaps.com/blog/pmtiles-v3-layout-compression/">directory encoding&lt;/a> delta-encodes TileIDs; if an archive is clustered, it can also effectively delta-encode offsets, since entries are contiguous (excluding the small % of de-duplicated tiles).&lt;/p>
&lt;p>Another way to view this optimization is as a compromise between &lt;em>sparse&lt;/em> and &lt;em>dense&lt;/em> directories. A &lt;em>sparse&lt;/em> representation of the directory is an associative map between TileID and offset+length:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">1&lt;/span>&lt;span style="color:#f92672">:&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>,&lt;span style="color:#ae81ff">100&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">2&lt;/span>&lt;span style="color:#f92672">:&lt;/span>[&lt;span style="color:#ae81ff">100&lt;/span>,&lt;span style="color:#ae81ff">100&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">3&lt;/span>&lt;span style="color:#f92672">:&lt;/span>[&lt;span style="color:#ae81ff">200&lt;/span>,&lt;span style="color:#ae81ff">100&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">4&lt;/span>&lt;span style="color:#f92672">:&lt;/span>[&lt;span style="color:#ae81ff">100&lt;/span>,&lt;span style="color:#ae81ff">100&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On the other hand, a &lt;em>dense&lt;/em> representation of the directory is an array, where the index in the array is the TileID:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>[[&lt;span style="color:#ae81ff">0&lt;/span>,&lt;span style="color:#ae81ff">100&lt;/span>],[&lt;span style="color:#ae81ff">100&lt;/span>,&lt;span style="color:#ae81ff">100&lt;/span>],[&lt;span style="color:#ae81ff">200&lt;/span>,&lt;span style="color:#ae81ff">100&lt;/span>],[&lt;span style="color:#ae81ff">100&lt;/span>,&lt;span style="color:#ae81ff">100&lt;/span>]]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Whether a sparse or dense data structure is optimal depends on the data being stored. A &lt;a href="http://lens.pathandfocus.com">Canada wildfires tileset&lt;/a> may be widely dispersed, where a sparse structure is better; or they might be OSM basemaps for a populous city that blanket an entire area of continuous tiles. A dense array-like format would ideal, but using it for wildfires means storing many directories full of zero or null entries.&lt;/p>
&lt;p>PMTiles uses a sparse directory layout, but by delta-encoding clustered entries, the on-disk bytes are much like a dense layout where only the offset is stored.&lt;/p>
&lt;h2 id="determinism">Determinism&lt;/h2>
&lt;p>A more significant advantage of clustered archives is that they make &lt;strong>archive layout deterministic:&lt;/strong> running a program on the same input twice should produce byte-for-byte identical output. Storage engines like SQLite, used in MBTiles, can create different output bytes based on order of operations or library version. Tileset creation is computationally expensive, so programs generate tiles on parallel CPU threads and may finish in indeterminate order. By defining a single, canonical ordering of tiles, detecting data changes in PMTiles works with &lt;a href="https://en.wikipedia.org/wiki/MD5">checksums like MD5&lt;/a>, and secure verification of outputs is possible with &lt;a href="https://en.wikipedia.org/wiki/SHA-2">SHA-256&lt;/a> or &lt;a href="https://en.wikipedia.org/wiki/BLAKE_(hash_function)">Blake3&lt;/a>.&lt;/p>
&lt;p>&lt;a href="https://github.com/msbarry">Mike Barry&lt;/a> has recently enhanced Planetiler&amp;rsquo;s determinism by ensuring vector tile contents like polygon ring ordering and tag ordering are consistent between runs.&lt;/p>
&lt;h2 id="pmtiles-verify">pmtiles verify&lt;/h2>
&lt;p>The &lt;a href="https://docs.protomaps.com/pmtiles/cli">pmtiles CLI&lt;/a> now includes a &lt;code>verify&lt;/code> operation that validates the contiguous Hilbert ordering of all tile contents, with no forward references.&lt;/p>
&lt;h2 id="recommendations">Recommendations&lt;/h2>
&lt;p>If your only use case is deploying a PMTiles file on S3 for web map visualization, clustered archives aren&amp;rsquo;t essential: they only minimize the space taken up by the directory.&lt;/p>
&lt;p>&lt;strong>Clustered archives are mandatory&lt;/strong> for more advanced operations on PMTiles, such as &lt;a href="https://docs.protomaps.com/pmtiles/cli#extract">extracting a subset&lt;/a> of tiles from a larger archive, which will be the subject of a future post.&lt;/p></description></item><item><title>Serverless Maps - Now Open Source</title><link>https://protomaps.com/blog/serverless-maps-now-open-source/</link><pubDate>Mon, 06 Mar 2023 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/serverless-maps-now-open-source/</guid><description>&lt;h2 id="accelerating-maps">Accelerating Maps&lt;/h2>
&lt;p>Last year I described &lt;a href="https://protomaps.com/blog/blog/serverless-self-hosted-maps/">Protomaps Mantle&lt;/a>, a commercial solution for self hosting planet-scale maps without running any servers. It&amp;rsquo;s built on top of the open, cloud optimized &lt;a href="https://protomaps.com/blog/docs/pmtiles">PMTiles&lt;/a> format, and adds the option of a content delivery network in front to accelerate map tiles to users anywhere in the world.&lt;/p>
&lt;p>&lt;strong>The code components of Mantle are now open source,&lt;/strong> under the same permissive licenses as the &lt;a href="https://github.com/protomaps">rest of the Protomaps software stack.&lt;/a>&lt;/p>
&lt;p>It&amp;rsquo;s also been renamed: you can find it all at the &lt;code>serverless/&lt;/code> directory of the &lt;a href="https://github.com/protomaps/PMTIles">PMTiles repository&lt;/a>, and get started with the docs at the new &lt;a href="https://protomaps.com/blog/docs/cdn">Protomaps Documentation site: CDN Integration.&lt;/a>&lt;/p>
&lt;p>A summary of features:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>AWS Lambda and Cloudflare Workers&lt;/strong> functions for turning Z/X/Y URLs into PMTiles range requests to private storage buckets. Under 10kb compressed, with copy-and-paste deployment.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://protomaps.com/blog/docs/cdn">Simple, 20-minute setup guides&lt;/a> on installing in your own AWS or Cloudflare account.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Tested in production at scale:&lt;/strong> On AWS, response times average 100 ms for cache hits, 200 ms for misses. &lt;em>Cloudflare is significantly slower, but is improving as Cloudflare&amp;rsquo;s R2 offering matures.&lt;/em>&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="used-in-production">Used in Production&lt;/h2>
&lt;p>&lt;a href="https://felt.com/blog/upload-anything">Felt&lt;/a> uses Protomaps with a CDN to serve user uploaded tilesets created with their open source tile generator, &lt;a href="https://github.com/felt/tippecanoe">Tippecanoe&lt;/a>.&lt;/p>
&lt;p>&lt;a href="https://openinframap.org/about">OpenInfraMap&lt;/a> uses the Protomaps Cloudflare integration to store and visualize rarely seen infrastructure from the OpenStreetMap dataset.&lt;/p>
&lt;p>&lt;a href="https://www.kschaul.com/post/2023/02/16/how-the-post-is-replacing-mapbox-with-open-source-solutions/">The Washington Post&lt;/a> uses the Protomaps integration with AWS Lambda and Cloudfront to affordably power &lt;a href="https://www.washingtonpost.com/business/interactive/2023/all-cash-buyers-housing-market/">interactive data stories&lt;/a>.&lt;/p>
&lt;h2 id="cost-comparison">Cost Comparison&lt;/h2>
&lt;p>A web application with 50,000 map loads using the Google Maps API will cost &lt;a href="https://mapsplatform.google.com/pricing/">$350 per month.&lt;/a>&lt;/p>
&lt;p>A similar amount of traffic running through a Protomaps CDN install on Cloudflare, with an average of 20 tiles per session, costs &lt;strong>fifty cents&lt;/strong> in &lt;a href="https://developers.cloudflare.com/workers/platform/pricing/">Cloudflare Workers request fees.&lt;/a>&lt;/p>
&lt;h2 id="free-datasets">Free Datasets&lt;/h2>
&lt;p>Protomaps-on-CDN enables serving any tiled data at scale - vector map tiles, raster tiles, or even RGB-encoded terrain data, all from a single serverless function. Here&amp;rsquo;s two free example datasets to try with your own install:&lt;/p>
&lt;h4 id="vector-tiles-zooms-0-10">Vector Tiles (zooms 0-10)&lt;/h4>
&lt;video controls preload="auto" width="100%" playsinline class="html-video">
 &lt;source src="https://protomaps.com/blog/blog/serverless-maps-now-open-source/vector.mp4" type="video/mp4">
 &lt;span>Your browser doesn't support embedded videos, but don't worry, you can &lt;a href="https://protomaps.com/blog/blog/serverless-maps-now-open-source/vector.mp4">download it&lt;/a> and watch it with your favorite video player!&lt;/span>
&lt;/video>
&lt;p>&lt;a href="https://protomaps.github.io/basemaps/index.html">Live Example Page&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://protomaps.com/blog/docs/downloads/basemaps">Link to download the dataset&lt;/a>&lt;/p>
&lt;p>&lt;strong>A global vector tile dataset based on OpenStreetMap data.&lt;/strong> The organization of layers and tags is described at &lt;a href="https://protomaps.com/blog/docs/frontends/basemap-layers">Basemap Layers&lt;/a>, and &lt;a href="http://github.com/protomaps/basemaps">open source styles&lt;/a> are provided for Leaflet as well as MapLibre GL vector rendering. It&amp;rsquo;s based on OpenStreetMap data only, so you can do anything you&amp;rsquo;d like with the data, subject to the &lt;a href="https://wiki.osmfoundation.org/wiki/Licence/Community_Guidelines/Produced_Work_-_Guideline">ODbL Produced Work attribution requrements.&lt;/a>&lt;/p>
&lt;h4 id="terrain-rgb">Terrain RGB&lt;/h4>
&lt;video controls preload="auto" width="100%" playsinline class="html-video">
 &lt;source src="https://protomaps.com/blog/blog/serverless-maps-now-open-source/rgb_terrain.mp4" type="video/mp4">
 &lt;span>Your browser doesn't support embedded videos, but don't worry, you can &lt;a href="https://protomaps.com/blog/blog/serverless-maps-now-open-source/rgb_terrain.mp4">download it&lt;/a> and watch it with your favorite video player!&lt;/span>
&lt;/video>
&lt;p>&lt;a href="https://protomaps.github.io/PMTiles/examples/maplibre_raster_dem.html">Live Example page&lt;/a>&lt;/p>
&lt;p>&lt;strong>A global terrain RGB dataset&lt;/strong>, packaged from the &lt;a href="https://github.com/tilezen/joerd">Mapzen Joerd&lt;/a> project. This is designed to work with MapLibre GL JS for global hillshading.&lt;/p>
&lt;p>See the &lt;a href="https://github.com/tilezen/joerd/blob/master/docs/attribution.md">Joerd README for attribution requirements.&lt;/a>&lt;/p>
&lt;h4 id="copy-these-datasets">Copy these datasets!&lt;/h4>
&lt;p>You can hotlink directly to these datasets, because they&amp;rsquo;re stored in public buckets on Cloudflare R2 and include &lt;code>Access-Control-Allow-Origin: *&lt;/code> headers. But be aware that these URLs might change in the future.&lt;/p>
&lt;p>A solution is to copy the PMTiles files to your own S3 or R2 bucket, so they&amp;rsquo;ll last forever, under your own control, and at minimal cost. Unlike a map SaaS, you can cache and remix these datasets however you like.&lt;/p>
&lt;p>Finally, if you&amp;rsquo;d just like to use the Protomaps-hosted deployment - it happens to run on Cloudflare - you can &lt;a href="https://protomaps.com/dashboard">register for an API key&lt;/a>. It&amp;rsquo;s free for non-commercial use, and commercial use is available to our &lt;a href="">GitHub Sponsors&lt;/a>.&lt;/p>
&lt;p>Happy mapping!&lt;/p></description></item><item><title>What's new in PMTiles V3</title><link>https://protomaps.com/blog/pmtiles-v3-whats-new/</link><pubDate>Mon, 31 Oct 2022 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/pmtiles-v3-whats-new/</guid><description>&lt;p>&lt;strong>PMTiles is a single-file archive format for map tiles&lt;/strong>, optimized for the cloud. Think about it like &lt;a href="https://github.com/mapbox/mbtiles-spec">MBTiles&lt;/a>, where the database can live on another computer or static storage like S3; or as a minimal alternative to &lt;a href="https://www.cogeo.org">Cloud Optimized GeoTIFFs&lt;/a> for any tiled data - remote sensing readings, photographs, or vector GIS features.&lt;/p>
&lt;p>Why adopt PMTiles? Companies like &lt;a href="https://felt.com/blog/upload-anything">Felt, a collaborative mapmaking app, are using PMTiles for user-uploaded datasets&lt;/a> - eliminating the need to run map tile servers at all.&lt;/p>
&lt;h2 id="spec-version-3">Spec version 3&lt;/h2>
&lt;p>&lt;a href="https://github.com/protomaps/PMTiles/blob/master/spec/v3/spec.md">Read the specification on GitHub&lt;/a>&lt;/p>
&lt;p>In its first year of existence, PMTiles focused on being the simplest possible implementation of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests">HTTP Byte Range&lt;/a> read strategy. &lt;strong>PMTiles V3&lt;/strong> is a revision that makes the retrieval and storage of tiles not just &lt;em>simple&lt;/em> but also &lt;em>efficient&lt;/em>. Minimizing archive size and the number of intermediate requests has a direct effect on the latency of tile requests and ultimately the end user experience of viewing a map on the web.&lt;/p>
&lt;h3 id="file-structure">File Structure&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>97% smaller overhead&lt;/strong> - Spec version 2 would always issue a 512 kilobyte initial request; version 3 reduces this to &lt;strong>16 kilobytes.&lt;/strong> What remains the same is that nearly any map tile can be retrieved in at most two additional requests.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Unlimited metadata&lt;/strong> - version 2 had a hard cap on the amount of JSON metadata of about 300 kilobytes; version 3 removes this limit. This is essential for tools like &lt;a href="http://github.com/felt/tippecanoe">tippecanoe&lt;/a> to store detailed column statistics. Essential archive information, such as tile type and compression methods, are stored in a binary header separate from application metadata.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Hilbert tile IDs&lt;/strong> - tiles internally are addressed by a single 64-bit Hilbert tile ID instead of Z/X/Y. See the &lt;a href="https://protomaps.com/blog/blog/pmtiles-v3-hilbert-tile-ids/">blog post on Tile IDs for details.&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Archive ordering&lt;/strong> - An optional &lt;code>clustered&lt;/code> mode enforces that tile contents are laid out in Tile ID order.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Compressed directories and metadata&lt;/strong> - Directories used to fetch offsets of tile data consume about 10% the space of those in version 2. See the &lt;a href="https://protomaps.com/blog/blog/pmtiles-v3-layout-compression">blog post on compressed directories&lt;/a> for details.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="javascript">JavaScript&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Compression&lt;/strong> - The TypeScript &lt;a href="https://github.com/protomaps/PMTiles/tree/master/js">pmtiles&lt;/a> library now includes a decompressor - &lt;a href="https://github.com/101arrowz/fflate">fflate&lt;/a> - to allow reading compressed vector tile archives directly in the browser. This reduces the size and latency of vector tiles by as much as 70%.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Tile Cancellation&lt;/strong> - All JavaScript plugins now support &lt;em>tile cancellation&lt;/em>, meaning quick zooming across many levels will interrupt the loading of tiles that are never shown. This has a significant effect on the perceived user experience, as tiles at the end of a animation will appear earlier.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>ETag support&lt;/strong> - clients can detect when files change on static storage by reading the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag">ETag&lt;/a> HTTP header. This means that PMTiles-based map applications can update datasets in place at low frequency without running into caching problems.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="inspector-app">Inspector app&lt;/h3>
&lt;p>&lt;a href="https://github.com/protomaps/PMTiles">PMTiles on GitHub&lt;/a> now hosts an open source inspector for local or remote archives. View an archive hosted on your cloud storage (CORS required) - or drag and drop a file from your computer - no server required.&lt;/p>
&lt;video controls preload="auto" width="100%" playsinline class="html-video">
 &lt;source src="https://protomaps.com/blog/blog/pmtiles-v3-whats-new/pmtiles_viewer.mp4" type="video/mp4">
 &lt;span>Your browser doesn't support embedded videos, but don't worry, you can &lt;a href="https://protomaps.com/blog/blog/pmtiles-v3-whats-new/pmtiles_viewer.mp4">download it&lt;/a> and watch it with your favorite video player!&lt;/span>
&lt;/video>
&lt;h3 id="leaflet">Leaflet&lt;/h3>
&lt;p>For raster tiles, there is first-class support for loading PNG or JPG image archives into Leaflet via the tiny (7 kilobytes!) &lt;code>PMTiles&lt;/code> library like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">p&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">pmtiles&lt;/span>.&lt;span style="color:#a6e22e">PMTiles&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;example.pmtiles&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">pmtiles&lt;/span>.&lt;span style="color:#a6e22e">leafletRasterLayer&lt;/span>(&lt;span style="color:#a6e22e">p&lt;/span>).&lt;span style="color:#a6e22e">addTo&lt;/span>(&lt;span style="color:#a6e22e">map&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For vector tiles, you&amp;rsquo;ll need to use &lt;a href="https://github.com/protomaps/protomaps.js">protomaps.js&lt;/a>, the from-scratch renderer built for vector rendering and labeling using plain Canvas. It&amp;rsquo;s only about 32 kilobytes - a fraction of the size of an alternative like MapLibre GL JS - and now supports V3 archives.&lt;/p>
&lt;h3 id="maplibre-gl-js">MapLibre GL JS&lt;/h3>
&lt;p>The MapLibre &lt;a href="https://maplibre.org/maplibre-gl-js-docs/api/properties/#addprotocol">protocol plugin&lt;/a> has a new, simpler API; specifying the archive under a source &lt;code>url&lt;/code> will automatically infer the archive&amp;rsquo;s &lt;code>minzoom&lt;/code> and &lt;code>maxzoom&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;sources&amp;#34;&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;example_source&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;vector&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;url&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;pmtiles://https://example.com/example.pmtiles&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="python">Python&lt;/h2>
&lt;p>&lt;a href="https://github.com/protomaps/PMTiles/tree/master/python">pmtiles/python on GitHub&lt;/a>&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Python libraries are now modular and can have data sources swapped out. A PMTiles file can be read from disk, or a custom function can be provided to grab byte ranges from AWS via the boto library, Google Cloud, or any other blob data source.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Python command line utilities have been deprecated as the first-class tooling for creating and working with PMTiles.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="go">Go&lt;/h2>
&lt;p>&lt;a href="http://github.com/protomaps/go-pmtiles">go-pmtiles on GitHub&lt;/a>&lt;/p>
&lt;p>The greatest obstacle to adopting PMTiles for many users was the need to have a working Python 3 installation on your computer.&lt;/p>
&lt;p>The official PMTiles tooling is now a single-file executable you can download at &lt;a href="https://github.com/protomaps/go-pmtiles/releases">GitHub Releases&lt;/a>.&lt;/p>
&lt;p>Example for converting an MBTiles archive:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pmtiles convert input.mbtiles output.pmtiles
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This will spit out some facts on the internals of your archive:&lt;/p>
&lt;pre tabindex="0">&lt;code>tippecanoe ne_10m_admin_1_states_provinces.geojsonseq -o ne_10m_admin_1_states_provinces.mbtiles -z8
pmtiles convert ne_10m_admin_1_states_provinces.mbtiles ne_10m_admin_1_states_provinces.pmtiles
...
# of addressed tiles: 40560
# of tile entries (after RLE): 20733
# of tile contents: 18933
Root dir bytes: 57
Leaves dir bytes: 53570
Num leaf dirs: 6
Total dir bytes: 53627
Average leaf dir bytes: 8928
Average bytes per addressed tile: 1.32
Finished in 444.930625ms
&lt;/code>&lt;/pre>&lt;p>The above shows that the sample dataset - &lt;a href="https://www.naturalearthdata.com/downloads/10m-cultural-vectors/">Admin 1 boundaries from Natural Earth&lt;/a> has more than 50% redundant tiles. Although about 40,000 tiles are addresses by the archive, only 19,000 tiles are stored.&lt;/p>
&lt;p>On average, only &lt;strong>1.3 bytes or 11 bits&lt;/strong> is needed per tile in the directory index after compression!&lt;/p>
&lt;p>To upgrade your PMTiles V2 archive to V3:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pmtiles convert input_v2.pmtiles output_v3.pmtiles
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Inspect a PMTiles V3 archive:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pmtiles show file://. output.pmtiles
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Uploading your archive to cloud storage, once you&amp;rsquo;ve put your credentials in environment variables:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pmtiles upload LOCAL.pmtiles &lt;span style="color:#e6db74">&amp;#34;s3://BUCKET_NAME?endpoint=https://example.com&amp;amp;region=region&amp;#34;&lt;/span> REMOTE.pmtiles
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ecosystem">Ecosystem&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>Bringing PMTiles support to &lt;a href="https://github.com/protomaps/PMTiles/issues/3">OpenLayers (GitHub issue #3)&lt;/a>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Luke Seelenbinder has started a implementation of &lt;a href="https://github.com/stadiamaps/pmtiles-rs">PMTiles in Rust&lt;/a>.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="free-downloads">Free Downloads&lt;/h2>
&lt;p>Finally, you can download OpenStreetMap-derived, up-to-the minute basemap tilesets from &lt;a href="https://protomaps.com/downloads">protomaps.com/downloads&lt;/a>, now only delivered in the V3 format. Small-area downloads are perfect for your hyper-local mapping project that will work forever, hosted on storage like GitHub Pages or S3.&lt;/p></description></item><item><title>PMTiles version 3: Disk Layout and Compressed Directories</title><link>https://protomaps.com/blog/pmtiles-v3-layout-compression/</link><pubDate>Fri, 12 Aug 2022 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/pmtiles-v3-layout-compression/</guid><description>&lt;p>&lt;a href="https://github.com/protomaps/PMTiles">PMTiles&lt;/a> is a single-file archive format for pyramids of tiled map data. The &lt;a href="https://protomaps.com/blog/blog/pmtiles-v3-hilbert-tile-ids">last post&lt;/a> described the new &lt;code>Entry&lt;/code> struct to compact repetitive data in-memory; the next step is to &lt;strong>further shrink directories for storage on disk and transfer over the Internet.&lt;/strong>&lt;/p>
&lt;p>PMTiles is designed to substitute for APIs like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>https://example.s3-like-storage.com/tileset/{z}/{x}/{y}.pbf
&lt;/code>&lt;/pre>&lt;p>One storage pattern is to store each tile as its own object, relying on cloud storage&amp;rsquo;s filesystem-like paths:&lt;/p>
&lt;pre tabindex="0">&lt;code> ┌───┐ 
┌────────┐ ┌───▶│███│ /tileset/1/0/0.pbf
│ │───┘ └───┘ 
│ │ ┌───┐ 
│ S3 │───────▶│███│ /tileset/1/0/1.pbf
│ │ └───┘ 
│ │───┐ ┌───┐ 
└────────┘ └───▶│███│ /tileset/1/1/1.pbf
 └───┘ 
&lt;/code>&lt;/pre>&lt;p>But this approach doesn&amp;rsquo;t scale up to planet-sized datasets, since millions of unique tiles can take days to upload.&lt;/p>
&lt;p>A PMTiles archive is a &lt;strong>single file&lt;/strong> upload:&lt;/p>
&lt;pre tabindex="0">&lt;code>https://example.s3-like-storage.com/tileset.pmtiles
&lt;/code>&lt;/pre>&lt;p>The information mapping a &lt;code>Z,X,Y&lt;/code> coordinate to the address of the tile must be stored in the file itself via an embedded &lt;code>Directory&lt;/code> sector. Making interactive maps fast and affordable means making this directory &lt;em>as small as possible&lt;/em>.&lt;/p>
&lt;pre tabindex="0">&lt;code>
┌────────┐ /tileset.pmtiles 
│ │ ┌─────────────────────┐
│ │ │1,0,0 ┌───┐┌───┐┌───┐│
│ S3 │───────▶ │1,0,1 │███││███││███││
│ │ │1,1,1 └───┘└───┘└───┘│
│ │ └─────────────────────┘
└────────┘ 
&lt;/code>&lt;/pre>&lt;p>&lt;strong>PMTiles v2&lt;/strong> punts on compression completely; it has a 1:1 relationship between the file layout and in-memory data structures. Waiting for half a megabyte of directory data for every map is slow, but the implementation remains dead simple and has been &lt;em>good enough&lt;/em> to prove out the design across diverse environments like Cloudflare Workers.&lt;/p>
&lt;p>The goal of the next specification version is not just to &lt;code>gzip&lt;/code> directories and call it a day, but &lt;strong>hand-tune a custom compression codec specific to map datasets.&lt;/strong> Projects like &lt;a href="https://parquet.apache.org">Apache Parquet&lt;/a> combine multiple compression techniques for arbitrary non-tiled data; our approach will look more like the domain-specific compression for &lt;a href="https://en.wikipedia.org/wiki/Portable_Network_Graphics#Compression">PNG images&lt;/a>, but tuned to map tiles instead of RGB pixels.&lt;/p>
&lt;h2 id="disk-layout">Disk Layout&lt;/h2>
&lt;p>PMTiles v2 did not enforce any particular ordering for tile contents in the archive, so it&amp;rsquo;s easy to generate archives with multi-threaded programs like &lt;a href="https://github.com/protomaps/tippecanoe">Tippecanoe&lt;/a>. &lt;strong>v3 adds an optional header field &lt;code>clustered&lt;/code>&lt;/strong>: a boolean indicating the disk layout of tile contents is ordered by Hilbert &lt;code>TileID&lt;/code>, analogous to &lt;a href="https://github.com/flatgeobuf/flatgeobuf">FlatGeobuf&amp;rsquo;s indexed layout.&lt;/a> A clustered archive enables optimized clients to batch tile requests for lower latency, inspired by Markus Tremmel&amp;rsquo;s &lt;a href="https://github.com/mactrem/com-tiles">COMTiles&lt;/a> project.&lt;/p>
&lt;pre tabindex="0">&lt;code>clustered=false clustered=true 
──────────────▶ ▼ ┌───┐ ┌───┐ ▲
─────▶ ───────▶ └─┘ ┌─┘ └─┐ └─┘
──▶ ──────────▶ ┌─┐ └─┐ ┌─┘ ┌─┐
──────────▶ ──▶ │ └───┘ └───┘ │
──────────────▶ └─┐ ┌─────┐ ┌─┘
───────▶ ─────▶ ┌─┘ └─┐ ┌─┘ └─┐
────▶ ────────▶ │ ┌─┐ │ │ ┌─┐ │
───────▶ ─────▶ └─┘ └─┘ └─┘ └─┘
&lt;/code>&lt;/pre>&lt;h2 id="test-dataset">Test Dataset&lt;/h2>
&lt;p>Our starting example is a global vector basemap tileset. It addresses 357,913,941 individual tiles, or every tile on every zoom level between 0 and 14. (It includes both an &lt;code>earth&lt;/code> and &lt;code>ocean&lt;/code> layer, so there are no holes.) After Hilbert run-length encoding, 40,884,468 &lt;code>Entry&lt;/code> records remain.&lt;/p>
&lt;p>A direct serialization of these records to disk is 40884468 * 24 bytes or &lt;strong>981.2 MB&lt;/strong>. Simple gzip compression reduces this to 305.4 MB, but we should be able to do better.&lt;/p>
&lt;h2 id="varint-encoding">Varint Encoding&lt;/h2>
&lt;p>A web-optimized tileset should have individual tiles under a megabyte in size, so 32 bits for &lt;code>Length&lt;/code> is overkill. We replace the fixed-size records with a stream of unsigned Varints. We also chop off unnecessary high bits used in &lt;code>TileId&lt;/code>, &lt;code>RunLength&lt;/code> and &lt;code>Offset&lt;/code>.&lt;/p>
&lt;p>This step reduces the &lt;strong>981.2 MB directory to 526.4 MB, or 53.6% of the original size.&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code> TileId RL Offset Len
┌───────────┬─────┬──────────┬────┐ ┌─────┬───┬───┬────┐ 
│ 100 │ 1 │ 0 │2200│ │ 100 │ 1 │ 0 │2200│ 
├───────────┼─────┼──────────┼────┤ ├─────┴┬──┴┬──┴───┬┴───┐ 
│ 101 │ 1 │ 2200 │2300│ │ 101 │ 1 │ 2200 │2300│ 
├───────────┼─────┼──────────┼────┤ ──────▶ ├──────┼───┼──────┴─┬──┴─┐ 
│ 103 │ 1 │ 4500 │2000│ │ 103 │ 1 │ 4500 │2000│ 
├───────────┼─────┼──────────┼────┤ ├──────┴┬──┴┬───────┴┬───┴┐
│ 104 │ 1 │ 6500 │1900│ │ 104 │ 1 │ 6500 │1900│
└───────────┴─────┴──────────┴────┘ └───────┴───┴────────┴────┘
&lt;/code>&lt;/pre>&lt;h2 id="delta-encoding-of-tileid--offset">Delta Encoding of TileID + Offset&lt;/h2>
&lt;p>Because a directory is sorted by ascending TileID, we can store deltas between consecutive entries instead of large numbers.&lt;/p>
&lt;p>In a &lt;code>clustered&lt;/code> archive, the physical layout of tile data will mostly match &lt;code>TileID&lt;/code> order. Where tile contents are contiguous, we can keep &lt;code>Length&lt;/code> while replacing &lt;code>Offset&lt;/code> with 0, since the &lt;code>Length&lt;/code> implies the delta to the next &lt;code>Offset&lt;/code>.&lt;/p>
&lt;p>Since this delta encoding makes values small, the varint step above should be even more effective.&lt;/p>
&lt;p>These two encodings reduce the &lt;strong>526.4 MB directory to 243.2 MB, or 24.8% of the original size.&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code>┌─────┬───┬───┬────┐ ┌─────┬───┬───┬────┐
│ 100 │ 1 │ 0 │2200│ │ 100 │ 1 │ 0 │2200│
├─────┴┬──┴┬──┴───┬┴───┐ ├───┬─┴─┬─┴─┬─┴──┬─┘
│ 101 │ 1 │ 2200 │2300│ │ 1 │ 1 │ 0 │2300│ 
├──────┼───┼──────┴─┬──┴─┐ ──────▶ ├───┼───┼───┼────┤ 
│ 103 │ 1 │ 4500 │2000│ │ 2 │ 1 │ 0 │2000│ 
├──────┴┬──┴┬───────┴┬───┴┐ ├───┼───┼───┼────┤ 
│ 104 │ 1 │ 6500 │1900│ │ 1 │ 1 │ 0 │1900│ 
└───────┴───┴────────┴────┘ └───┴───┴───┴────┘ 
&lt;/code>&lt;/pre>&lt;h2 id="column-transpose">Column transpose&lt;/h2>
&lt;p>Instead of storing each entry in order, we transpose the values to a columnar layout.&lt;/p>
&lt;pre tabindex="0">&lt;code>┌─────┬───┬───┬────┐ ┌─────┬───┬───┬───┐ 
│ 100 │ 1 │ 0 │2200│ │ 100 │ 1 │ 2 │ 1 │ 
├───┬─┴─┬─┴─┬─┴──┬─┘ ├───┬─┴─┬─┴─┬─┴─┬─┘ 
│ 1 │ 1 │ 0 │2300│ │ 1 │ 1 │ 1 │ 1 │ 
├───┼───┼───┼────┤ ──────▶ ├───┼───┼───┼───┤ 
│ 2 │ 1 │ 0 │2000│ │ 0 │ 0 │ 0 │ 0 │ 
├───┼───┼───┼────┤ ├───┴┬──┴─┬─┴──┬┴───┐
│ 1 │ 1 │ 0 │1900│ │2200│2300│2000│1900│
└───┴───┴───┴────┘ └────┴────┴────┴────┘
&lt;/code>&lt;/pre>&lt;p>This step in isolation does not reduce the size of our directory. However, sparse geographic datasets will have repeated deltas of &lt;code>1&lt;/code>, &lt;code>RunLength=0&lt;/code> and &lt;code>Offset&lt;/code> zeroed in the first step, which aids in the next compression step.&lt;/p>
&lt;h2 id="general-purpose-compression">General-purpose compression&lt;/h2>
&lt;p>Finally, a general purpose compression algorithm like &lt;code>gzip&lt;/code> is applied to the transposed directory.&lt;/p>
&lt;p>This step reduces our &lt;strong>243.2 MB directory size to 91.6 MB, 9.3% of the original size&lt;/strong>. Without the column transpose above, the result is 102.0 MB.&lt;/p>
&lt;p>Compressors like Brotli and Zstandard improve on &lt;code>gzip&lt;/code> and are supported by the spec for when they&amp;rsquo;re widely available in browsers.&lt;/p>
&lt;h2 id="conclusions--next-steps">Conclusions + Next Steps&lt;/h2>
&lt;p>Our real-world, planet-scale dataset can address over 350 million individual tiles in just 91.6 megabytes, &lt;strong>beating generic compression by a factor of 3.&lt;/strong>&lt;/p>
&lt;p>The finishing touches to header design and directory partitoning are &lt;a href="https://github.com/protomaps/PMTiles/issues">under discussion on GitHub&lt;/a> and will be presented at the &lt;a href="https://talks.osgeo.org/foss4g-2022/talk/WXJKDM/">FOSS4G 2022 conference in Firenze, Italy&lt;/a>, along with a richer tool ecosystem for PMTiles.&lt;/p></description></item><item><title>PMTiles version 3: Hilbert Tile IDs and Run-Length Encoding</title><link>https://protomaps.com/blog/pmtiles-v3-hilbert-tile-ids/</link><pubDate>Tue, 09 Aug 2022 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/pmtiles-v3-hilbert-tile-ids/</guid><description>&lt;p>&lt;a href="https://github.com/protomaps/PMTiles">PMTiles&lt;/a> is a single-file archive format for pyramids of tile data, like those used to power interactive maps. It&amp;rsquo;s designed to make storage and serving of planet-scale tiled maps simple using only affordable S3-like storage and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests">HTTP Range Requests&lt;/a>.&lt;/p>
&lt;p>The physical world depicted in maps is &lt;a href="https://en.wikipedia.org/wiki/Coastline_paradox">infinitely detailed&lt;/a>, but also not random like white noise. By exploiting redundancy in the physical world, we can make specialized formats for GIS more compact and efficient.&lt;/p>
&lt;p>&lt;img src="PowersOfTen_sparse.jpg" alt="Powers of Ten">
&lt;a href="https://www.youtube.com/watch?v=0fKBhvDjuy">Image: © 1977 EAMES OFFICE LLC&lt;/a>&lt;/p>
&lt;p>&lt;em>The universe is sparse at all scales: dense clusters of unique features, with wide swaths of similarity&lt;/em>&lt;/p>
&lt;p>One step towards this in the first public version of PMTiles (specification version 2) was &lt;a href="https://protomaps.com/blog/blog/dynamic-maps-static-storage/#a-map-is-just-a-video">tile deduplication&lt;/a>. The set of all OpenStreetMap PNG tiles will have many references to the same blue square representing the ocean.&lt;/p>
&lt;p>Deploying PMTiles v2 for further real-world use cases and datasets has resulted in a few refinements to the design, the first of which is related to the &lt;strong>internal tile addressing format for version 3.&lt;/strong> The new specification is being finalized and &lt;a href="https://github.com/protomaps/PMTiles/issues">discussed on GitHub&lt;/a> in time for the &lt;a href="https://talks.osgeo.org/foss4g-2022/talk/WXJKDM/">FOSS4G conference happening this year in Firenze, Italy&lt;/a>.&lt;/p>
&lt;h2 id="old-vs-new-design">Old vs. New Design&lt;/h2>
&lt;p>&lt;strong>Refresher:&lt;/strong> the existing &lt;code>Entry&lt;/code> record in PMTiles version 2 looks like this, inspired by the popular &lt;a href="https://github.com/mapbox/mbtiles-spec">MBTiles SQLite-based format&lt;/a>:&lt;/p>
&lt;pre tabindex="0">&lt;code>uint8 Z
uint24 X
uint24 Y
uint48 Offset
uint32 Length
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Example:&lt;/strong> the entry &lt;code>Z=8 X=40 Y=87&lt;/code> points to byte range &lt;code>offset=4240340 length=32836&lt;/code> in a PMTiles file, which is a 32.7 kB PNG image near Vancouver, British Columbia:&lt;/p>
&lt;p>&lt;img src="8_40_87.png" alt="Image of Vancouver">&lt;/p>
&lt;p>&lt;a href="https://www.openstreetmap.org/copyright">© OpenStreetMap&lt;/a>&lt;/p>
&lt;p>The v3 design simplifies the &lt;code>Entry&lt;/code> struct to fewer and more standard column types:&lt;/p>
&lt;pre tabindex="0">&lt;code>uint64 TileId
uint32 RunLength
uint64 Offset
uint32 Length
&lt;/code>&lt;/pre>&lt;p>The equivalent of the above in the new design would be &lt;code>TileId=36052 RunLength=1 Offset=4240340 Length=32836&lt;/code>.&lt;/p>
&lt;h2 id="tileid">TileID&lt;/h2>
&lt;p>The &lt;code>TileId&lt;/code> 36052 corresponds to the Z,X,Y position of &lt;code>8,40,87&lt;/code>. The calculation of ID uses a &lt;strong>pyramid of Hilbert curves&lt;/strong> starting at &lt;code>TileId=0&lt;/code> for zoom level 0. The next zoom level, a 2x2 square, occupies the next four IDs in the ID space &lt;code>TileId=(1,2,3,4)&lt;/code>, the next level being the next 16 IDs, and so on.&lt;/p>
&lt;p>&lt;img src="pmtiles_v3_hilbert.png" alt="hilbert">&lt;/p>
&lt;p>In the example British Columbia tile above, the &lt;code>Z,X,Y&lt;/code> position of &lt;code>8,40,87&lt;/code> corresponds to the 14207th Hilbert curve position on level 8, which starts at &lt;code>TileID=21845&lt;/code>.&lt;/p>
&lt;p>The &lt;code>TileId&lt;/code> is a convenient 64-bit integer that packs low zoom levels into the least significant bits. This makes it efficient to store in bitmap representations like &lt;a href="https://roaringbitmap.org">Roaring&lt;/a>, &lt;a href="https://en.wikipedia.org/wiki/LEB128">variable-length integer encodings&lt;/a>, or constrained runtimes like JavaScript. The maximum representable tile zoom level in JavaScript numbers is 28.&lt;/p>
&lt;h2 id="runlength">RunLength&lt;/h2>
&lt;p>PMTiles v3 replaces &lt;code>Z,X,Y&lt;/code> with the single &lt;code>TileID&lt;/code> field, and also adds a new field &lt;code>RunLength&lt;/code>.&lt;/p>
&lt;p>The previous PMTiles design deduplicates repetitive tiles like blue ocean squares via multiple entries pointing to the same data. However, it can repeat the same entry &lt;strong>millions of times&lt;/strong> for large regions of oceans, which cover &lt;a href="https://en.wikipedia.org/wiki/Ocean">70% of the planet&lt;/a>.&lt;/p>
&lt;p>Ocean tiles are not only repetitive, but sparse and often &lt;strong>contiguous in Hilbert space.&lt;/strong> This entry:&lt;/p>
&lt;p>&lt;code>TileID=2578427,RunLength=107977,Offset=3650651795,Length=42&lt;/code>&lt;/p>
&lt;p>means that the 44 byte &lt;a href="https://en.wikipedia.org/wiki/Vector_tiles">vector tile&lt;/a> with a single square in the layer &lt;code>ocean&lt;/code> is repeated over 100,000 times, starting at &lt;code>Z,X,Y=11,285,1311&lt;/code> and ending at &lt;code>11,19,1304&lt;/code>. This single contiguous sequence is represented by the yellow-green gradient in the planet image below:&lt;/p>
&lt;p>&lt;img src="z11_ocean.png" alt="ocean">&lt;/p>
&lt;p>What previously required 107,977 separate directory entries, or 1.8 megabytes of storage, can now be repesented by a single 24 byte entry. This specific case of a &lt;strong>75,000x compression ratio&lt;/strong> is useful to minimize storage costs and transfer latency of map tiles, which can help developers build and deploy faster, more flexible maps.&lt;/p>
&lt;p>&lt;em>The next post will address the design of compressed directories in PMTiles version 3.&lt;/em>&lt;/p>
&lt;p>&lt;img src="pmtiles_v3_hilbert_all.jpg" alt="contiguous runs">&lt;/p>
&lt;p>&lt;em>Contiguous Hilbert runs&lt;/em>&lt;/p></description></item><item><title>Serverless Maps using Lambda or Cloudflare Workers</title><link>https://protomaps.com/blog/serverless-self-hosted-maps/</link><pubDate>Fri, 08 Apr 2022 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/serverless-self-hosted-maps/</guid><description>&lt;p>&lt;strong>Note: Mantle has been open-sourced and renamed, please see the &lt;a href="https://protomaps.com/blog/blog/serverless-maps-now-open-source">latest blog post!&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;p>In my &lt;a href="https://protomaps.com/blog/blog/free-tier-maps/">last post&lt;/a>, I described the downsides of the existing “pay-as-you-go” business model for maps. Organizations searching for a map solution weigh the benefits and drawbacks of &lt;em>build vs. buy&lt;/em>. Buying a hosted SaaS — and letting someone else manage infrastructure for you — makes sense when DIY map infrastructure looks like this diagram:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="infra_before.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>In this post, I&amp;rsquo;ll introduce &lt;strong>Mantle, the commercial offering of the Protomaps system.&lt;/strong> It&amp;rsquo;s the same backend that powers the interactive Leaflet maps on this site, like at &lt;a href="https://protomaps.com/downloads/large_map">Protomaps Downloads&lt;/a>. It enables scalable hosting of map data with two moving parts:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="infra_after.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;h2 id="runs-in-your-own-cloud-account">Runs in your own cloud account&lt;/h2>
&lt;p>Mantle is a commercial product bundling the &lt;a href="https://github.com/protomaps">open source components of the Protomaps ecosystem&lt;/a> into a &lt;strong>planet-scale, deployment-friendly solution for organizations.&lt;/strong>&lt;/p>
&lt;p>It has a mere two components:&lt;/p>
&lt;ul>
&lt;li>A &lt;strong>serverless app&lt;/strong> that runs on either AWS Lambda @ Edge or Cloudflare Workers.&lt;/li>
&lt;li>A &lt;strong>global basemap vector tile dataset&lt;/strong> derived from OpenStreetMap and delivered as a single 62 GB &lt;a href="https://github.com/protomaps/PMTiles">PMTiles&lt;/a> archive that lives on S3 or compatible storage.&lt;/li>
&lt;/ul>
&lt;p>With this combination, you get near-infinite storage space and scaling, edge caching for low latency, and security with SSL and CORS, &lt;strong>powered by your existing cloud account and CDN.&lt;/strong>&lt;/p>
&lt;h2 id="mantle-is-not-a-saas">Mantle is not a SaaS&lt;/h2>
&lt;p>I&amp;rsquo;ll emphasize here that this solution is &lt;strong>not Software as a Service (SaaS).&lt;/strong> It is licensed for a flat fee and self-hosted. It lives in your own AWS or Cloudflare account, but involves zero server management, databases or certificates.&lt;/p>
&lt;p>The software industry has shifted to hosted services because the diversity of on-premise installation induces fractal complexity. &amp;ldquo;Serverless&amp;rdquo; computing offers an elegant path forward: Mantle takes less than an hour to set up in your own account, and deployment of the CDN code is merely pasting a JavaScript snippet:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="lambda_deploy.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;h2 id="differentiation-in-maps">Differentiation in Maps&lt;/h2>
&lt;p>&lt;strong>The market for map APIs is crowded.&lt;/strong> Google Maps pioneered selling a hosted API to developers, and has maintained its lead in data completeness and quality. Rivals to the Google Maps API such as MapBox and even AWS and Microsoft offer more affordable plans, but stick to the &lt;a href="https://protomaps.com/blog/blog/free-tier-maps/">pay-as-you-go business model&lt;/a> with a focus on the most profitable market segments.&lt;/p>
&lt;p>While the Protomaps system has a lot of catching up to do in cartographic refinement, it has a key &lt;a href="https://hbr.org/2015/12/what-is-disruptive-innovation">disruptive&lt;/a> advantage over incumbents. A Mantle installation enables you to serve &lt;strong>unlimited map tile traffic&lt;/strong>, incurring only the costs of the underlying storage and CDN. Unlike hosted services, you&amp;rsquo;re free to cache, transform, and re-combine the data as you see fit for your application with no vendor lock-in.&lt;/p>
&lt;p>To illustrate the cost savings, every additional 1,000 users that load a map on &lt;a href="https://mapsplatform.google.com/pricing/">Google Maps costs 7 USD&lt;/a>. An additional million hits to &lt;a href="https://workers.cloudflare.com">Cloudflare Workers&lt;/a> costs fifteen cents. Even taking into account the additional cost of storage and bandwidth, there&amp;rsquo;s a &lt;strong>10x to 100x difference in total costs.&lt;/strong>&lt;/p>
&lt;h2 id="pricing">Pricing&lt;/h2>
&lt;p>The introductory pricing for Mantle is a &lt;strong>flat, per-site license of 950 USD a year&lt;/strong>, for integration into one End Product. You can continue to run Mantle after your one-year expires, but won&amp;rsquo;t have access to updated software, datasets or support.&lt;/p>
&lt;h2 id="funding-open-source">Funding Open Source&lt;/h2>
&lt;p>Mantle is a capstone product for the &lt;a href="http://github.com/protomaps">ecosystem of Protomaps tools&lt;/a>, covering a spectrum of technolology from &lt;a href="https://github.com/protomaps/OSMExpress">spatial databases&lt;/a> to &lt;a href="https://github.com/protomaps/protomaps.js">TypeScript map rendering&lt;/a>, as well as free, ODbL packaged geodata at &lt;a href="https://protomaps.com/downloads">Protomaps Downloads&lt;/a>. All software components are published under commercial-friendly, permissive BSD licenses, and you can assemble a production-ready system from them.&lt;/p>
&lt;p>Protomaps is currently a single-person, bootstrapped business, which removes the constraints of a typical tech startup. My intent is for the majority of mapping users — hobbyists, NGOs, corporations, everything in between — to use it completely for free. A first commercial offering is the &lt;strong>entry point for businesses to support the development of this open source ecosystem, influence its future, and get expert support&lt;/strong> for their own environments and datasets.&lt;/p>
&lt;h2 id="get-started">Get Started&lt;/h2>
&lt;p>Mantle has been tested by early users for a few months, and I&amp;rsquo;m gradually onboarding new customers with personalized support. If you&amp;rsquo;d like to take advantage of the introductory pricing, fill out &lt;a href="https://forms.gle/AdLVh4W8pwvNEZQS7">this short form&lt;/a> and I&amp;rsquo;ll get in contact via email.&lt;/p></description></item><item><title>Rethinking the Free Tier for Maps</title><link>https://protomaps.com/blog/free-tier-maps/</link><pubDate>Wed, 23 Mar 2022 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/free-tier-maps/</guid><description>&lt;p>Protomaps is a &lt;strong>new mapping system&lt;/strong>, and usually that implies a new map &lt;em>API&lt;/em> - an HTTP service, hosted by a company somewhere Out There on the Web. Your typical interactive web map talks to an API using these conventions:&lt;/p>
&lt;ol>
&lt;li>The world is split into a pyramid of square tiles, with objects simplified to one of many pre-determined zoom levels.&lt;/li>
&lt;li>The browser fetches an image or other data tile as the user zooms and pans.&lt;/li>
&lt;/ol>
&lt;video controls preload="auto" width="100%" playsinline class="html-video">
 &lt;source src="https://protomaps.com/blog/blog/free-tier-maps/tile_example.mp4" type="video/mp4">
 &lt;span>Your browser doesn't support embedded videos, but don't worry, you can &lt;a href="https://protomaps.com/blog/blog/free-tier-maps/tile_example.mp4">download it&lt;/a> and watch it with your favorite video player!&lt;/span>
&lt;/video>
&lt;p>Front-ends like &lt;a href="http://leafletjs.com">Leaflet&lt;/a> or &lt;a href="http://openlayers.org">OpenLayers&lt;/a> are mature libraries for consuming tiles, but leave unanswered &lt;em>where the tiles come from&lt;/em> and &lt;em>whether or not those tiles cost money&lt;/em>.&lt;/p>
&lt;h2 id="the-status-quo-consumption-pricing">The Status Quo: Consumption Pricing&lt;/h2>
&lt;p>Map tiles from an API are just one of many developer Software-as-a-Service (SaaS) products, which onboard new customers like this:&lt;/p>
&lt;ol>
&lt;li>Sign up and generate an API key.&lt;/li>
&lt;li>Embed that key on your site, which you can use for free up to a certain limit.&lt;/li>
&lt;li>Once you&amp;rsquo;ve exceeded that limit, you&amp;rsquo;re billed on a &lt;em>pay-as-you-go&lt;/em> pricing model.&lt;/li>
&lt;/ol>
&lt;p>This pay-as-you-go model is also called &lt;strong>consumption pricing&lt;/strong>. Good examples of this model in the developer market are &lt;a href="http://sentry.io">Sentry&lt;/a> for error tracking and &lt;a href="http://twilio.com">Twilio&lt;/a> for SMS. Google applied this model to Maps, and it is infamously expensive:&lt;/p>
&lt;video controls preload="auto" width="100%" playsinline class="html-video">
 &lt;source src="https://protomaps.com/blog/blog/free-tier-maps/google_maps_scale.mp4" type="video/mp4">
 &lt;span>Your browser doesn't support embedded videos, but don't worry, you can &lt;a href="https://protomaps.com/blog/blog/free-tier-maps/google_maps_scale.mp4">download it&lt;/a> and watch it with your favorite video player!&lt;/span>
&lt;/video>
&lt;p>&lt;strong>In theory, consumption pricing is good for both vendors and customers.&lt;/strong> It lets companies capture mindshare via a free tier: developers, more than anyone else, love free stuff, especially when learning new tools for personal side projects. It also lets them charge much, much more for use cases with &lt;em>high willingness to pay&lt;/em>, such as heavily trafficked retail websites, popular social media applications, or enterprise deployments.&lt;/p>
&lt;h2 id="why-saas-does-not-serve-all-map-use-cases">Why SaaS does not serve all map use cases&lt;/h2>
&lt;p>Most developer services that come to mind fit the consumption pricing model perfectly - indie projects pay nothing, startup companies buy a medium-sized plan, and heavy usage is priced in after clicking the &lt;em>Contact Us&lt;/em> button. However, a lot of &lt;strong>mapping use cases do not fit the consumption pricing model.&lt;/strong>&lt;/p>
&lt;p>Firstly, many useful applications of maps, even those that are &lt;em>important infrastructure for society&lt;/em>, have high traffic but low ability to pay, like civic technology projects, environment &amp;amp; climate, public health, journalism and information visualization. Projects are often built on the free tier of a map API but discover they can&amp;rsquo;t afford a higher tier after launch.&lt;/p>
&lt;p>Secondly, the SaaS model means the vendor must track usage, which means applications &lt;strong>must always connect to the public Internet&lt;/strong>. Developers &lt;em>could&lt;/em> cache, proxy or scrape map data for offline use, but this is always forbidden by terms of service - see the &lt;a href="https://developers.google.com/maps/terms-20180207#10.-license-restrictions">Google Maps TOS&lt;/a> as an example - because it contradicts the pay-as-you-go business model. Entire classes of applications are impossible due to policy, not technical feasibility. Mapping applications are always dependent on a third party, and exposed to changes in pricing or companies going offline on an &lt;a href="https://ourincrediblejourney.tumblr.com">Incredible Journey&lt;/a>.&lt;/p>
&lt;p>The narrow market served by pay-as-you-go pricing means that map vendors must cater to the most profitable commercial ventures, ignore exciting but unlucrative use cases, and enforce artificial scarcity of information.&lt;/p>
&lt;p>Lastly, vendors rely on &lt;strong>convincing customers that the service they provide is too difficult and complex to run themselves.&lt;/strong> Metered SaaS reshapes the architecture of systems to fit a business model. It creates strong disincentives for the creation of simple, modular software that can be adapted and re-used.&lt;/p>
&lt;h2 id="can-apis-be-totally-free">Can APIs be totally free?&lt;/h2>
&lt;p>&lt;strong>OpenStreetMap&lt;/strong> is thought of as a &amp;ldquo;free&amp;rdquo; map service, due to ubiquitous confusion between the cartographic product served from &lt;em>openstreetmap.org&lt;/em>, and the &lt;a href="http://planet.openstreetmap.org">OpenStreetMap dataset&lt;/a>. You&amp;rsquo;ve probably seen an embedded OSM map that looks like this:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="osm_carto.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>OSM through its &lt;a href="https://wiki.osmfoundation.org/wiki/Main_Page">Foundation&lt;/a> pays to host this for free, but for the primary purpose of aiding map editing and data creation, not widespread public embedding. The &lt;a href="https://operations.osmfoundation.org/policies/tiles/">Tile Usage Policy&lt;/a> plainly states: &lt;em>&amp;ldquo;OpenStreetMap data is free for everyone to use. Our tile servers are not.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>There does exist alternate map services like &lt;a href="http://maps.stamen.com">maps.stamen.com&lt;/a> that provide a set of pre-rendered map images. But free services ultimately depend on the a sponsor willing to commit to the long-term cost of data transfer, because bandwidth costs are not trivial.&lt;/p>
&lt;h2 id="holding-the-bandwidth-bag">Holding the Bandwidth Bag&lt;/h2>
&lt;p>Free map hosting is difficult for the same reasons image and video hosting is difficult: letting anyone embed assets anywhere on the web leads to exploding bandwidth costs. Bandwidth, while a commodity, is not free - AWS charges about $0.09 per gigabyte.&lt;/p>
&lt;p>Developers might believe that companies like Cloudflare offer unlimited bandwidth, but this is limited to HTML content and not media as described in &lt;a href="https://www.cloudflare.com/terms/">section 2.8&lt;/a> of their Terms of Use. Rented bare-metal servers can come with unmetered network connections, but the total transfer is limited by speed, not quantity - incoming requests can saturate a single network interface but no further.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="cloudflare-network.webp" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;a href="https://bdon.org/blog/web-map-performance/">Slippy maps are sensitive to latency&lt;/a>, so ideally a map can be served from a content delivery network, minimizing the time to serve a map to the end user. CDNs themselves follow consumption models, but strong competition between major players drives pricing down over time.&lt;/p>
&lt;h2 id="how-protomaps-is-different">How Protomaps Is Different&lt;/h2>
&lt;p>Protomaps, runs a &lt;a href="https://protomaps.com/blog/docs">public API&lt;/a> on the fast Cloudflare CDN, which you can use for free up to a soft cap of 1,000,000 requests per month. Like everywhere else, you can start by &lt;a href="https://protomaps.com/dashboard">signing up for an account&lt;/a> which will generate an associated API key.&lt;/p>
&lt;p>But here&amp;rsquo;s a key difference: If I change this free tier in the future, I go out of business, you outgrow the service, or you just don&amp;rsquo;t want to depend on an external service anymore, you can &lt;strong>export the map data and host it yourself&lt;/strong> for a limited area via &lt;a href="https://protomaps.com/downloads">Protomaps Downloads&lt;/a>. This is much faster than scraping the API, and wraps the map into a tidy &lt;a href="https://github.com/protomaps/PMTiles">PMTiles&lt;/a> package that lives on S3 with no intermediate server.&lt;/p>
&lt;p>Obviously, there is no way for me to ever know how much you&amp;rsquo;re using a downloaded map, and thus no way for me to charge you based on that! You&amp;rsquo;ll still pay for your own storage and bandwidth, but those will be orders of magnitude cheaper than typical metered map APIs. Your map will work forever, even offline, because it transforms the map from a live service into &lt;a href="https://protomaps.com/blog/blog/dynamic-maps-static-storage/">just another asset like an image or video&lt;/a>.&lt;/p>
&lt;p>If you&amp;rsquo;re excited about the possibilities here for your project, you can contact me at &lt;a href="mailto:brandon@protomaps.com">brandon@protomaps.com&lt;/a>.&lt;/p></description></item><item><title>The Surprising Anatomy of a One-Man Map Tech Stack</title><link>https://protomaps.com/blog/one-man-map-tech-stack/</link><pubDate>Thu, 06 Jan 2022 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/one-man-map-tech-stack/</guid><description>&lt;p>Protomaps is a &lt;strong>independent project to build a new map of the world.&lt;/strong> This scale of mission demands a wide range of &lt;a href="https://protomaps.com/blog/blog/new-way-to-make-maps/">novel frontend and backend components&lt;/a>, like an &lt;a href="https://github.com/protomaps/OSMExpress">open source spatial database&lt;/a>, a serverless &lt;a href="https://github.com/protomaps/PMTiles">tile archive format&lt;/a>, and a &lt;a href="https://github.com/protomaps/protomaps.js">vector map renderer&lt;/a>. There&amp;rsquo;s also a web application — the one you&amp;rsquo;re looking at now — with subsystems to process background tasks, ingest metrics and manage objects on cloud storage. Finally, there&amp;rsquo;s the core map engine for &lt;a href="https://en.wikipedia.org/wiki/Cartographic_generalization">cartographic generalization&lt;/a> and tiling of OpenStreetMap data, which you can access at &lt;a href="https://protomaps.com/downloads">Protomaps Downloads&lt;/a>.&lt;/p>
&lt;p>Bringing a viable product to market, in addition to publishing &lt;a href="http://github.com/protomaps">open source software&lt;/a>, creates some special pressures for a one-man shop. Chief among those is to make &lt;strong>conservative technology choices.&lt;/strong> Building on stable tech means I can spend my risk budget on unique parts of the project instead of libraries; it means my open source components can be adopted by others with minimum friction and mature independently of my own use case. Rust and WebAssembly are exciting for maps, but a bootstrapped company can&amp;rsquo;t afford multi-year investments in every emerging low-level technology. In this context, some Protomaps choices raise eyebrows among web and GIS developers, so I&amp;rsquo;ll document a few of them here.&lt;/p>
&lt;h2 id="c">C++&lt;/h2>
&lt;div style="text-align: center">
 &lt;img src="1970s_tech.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>C++ is the primary language at the heart of the Protomaps mapping system.&lt;/p>
&lt;p>C++ isn&amp;rsquo;t valued here for any particular properties of the language itself. In aspects like consistent programming conventions or package management, it feels miserable compared to newer languages.&lt;/p>
&lt;p>If you&amp;rsquo;re working with the nitty-gritty bits of geodata and computational geometry, &lt;strong>C++ should be your first choice&lt;/strong>, because it&amp;rsquo;s what the vast majority of the ecosystem is already written in. You&amp;rsquo;ll have access to &lt;em>decades&lt;/em> of battle-tested, &lt;a href="https://en.wikipedia.org/wiki/Numerical_stability">robust&lt;/a> libraries like &lt;a href="https://github.com/g-truc/glm">glm&lt;/a> for vector and matrix operations, &lt;a href="https://gdal.org">GDAL/OGR&lt;/a> for reading and writing geospatial data, &lt;a href="https://s2geometry.io">S2 Geometry&lt;/a> for spatial indexing on the sphere, and &lt;a href="http://www.angusj.com/delphi/clipper.php">Clipper&lt;/a> for computational geometry.&lt;/p>
&lt;p>If you&amp;rsquo;re using a different language, you can write a from-scratch alternative to those libraries — which for each would be a multi-year investment without immediate payoff — or use wrappers, which might offer a mismatched API or lead you down a debugging hole that would not exist if you just interfaced with C or C++. In addition, a core goal of Protomaps is to make &lt;strong>tooling that works at all cartographic scales, from a neighborhood to the entire planet.&lt;/strong> Using C++ and powerful libraries directly is the best way to efficiently process hundreds of millions of geographic features.&lt;/p>
&lt;h2 id="lmdb">LMDB&lt;/h2>
&lt;p>All serious data storage and manipulation inside Protomaps uses &lt;a href="https://www.symas.com/lmdb">Symas LMDB&lt;/a>.&lt;/p>
&lt;p>Most developers haven&amp;rsquo;t heard of LMDB, yet it&amp;rsquo;s the rock-solid foundation that contributes the most to making Protomaps work at scale. It&amp;rsquo;s an open source, multiprocess and fully ACID storage engine with some unique constraints, such as a single write transaction happening at a time. These constraints might limit it for general web applications but are perfect for Protomaps geodata, in which the only writes come from the &lt;a href="https://planet.openstreetmap.org">OpenStreetMap replication stream&lt;/a>.&lt;/p>
&lt;h4 id="making-lmdb-spatial">Making LMDB Spatial&lt;/h4>
&lt;p>LMDB is an embedded storage engine for binary blobs, and is optimized for 64-bit integer keys. It has special functions for &lt;a href="http://www.lmdb.tech/doc/group__mdb.html#ga1206b2af8b95e7f6b0ef6b28708c9127">key-prefix cursor traversal&lt;/a>. Protomaps uses spatial indexing schemes like &lt;a href="https://s2geometry.io/devguide/s2cell_hierarchy">S2 Cells&lt;/a> where geographic features are bucketed into cells identified by a 64-bit integer, and cells share binary prefixes based on parent-child relationships.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="hilbert.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;em>cells on the hilbert curve by &lt;a href="https://observablehq.com/@enjalot/hilbert-curve-plot">@enjalot&lt;/a>&lt;/em>&lt;/p>
&lt;h4 id="memory-mapping-matters">Memory-Mapping Matters&lt;/h4>
&lt;p>LMDB is memory-mapped as a fundamental fact of its design. The relevance of this beyond an implementation detail is not immediately obvious, but is revealed when paired with a system such as &lt;a href="https://capnproto.org">Cap&amp;rsquo;n Proto&lt;/a> or &lt;a href="https://google.github.io/flatbuffers/">FlatBuffers&lt;/a>. Geographic features can be indexed by compact 64-bit keys, and attributes or geometry accessed with a pointer into virtual memory — a &lt;a href="https://en.wikipedia.org/wiki/Zero-copy">zero-copy operation&lt;/a> — instead of wasting memory and CPU cycles on a deserialization step.&lt;/p>
&lt;p>Geodata access patterns exhibit locality in geographic space or ID-space; arranging IDs on space-filling curves means high cache hit rates with &lt;em>zero application code&lt;/em>, since virtual memory paging is handled by kernels.&lt;/p>
&lt;h4 id="lmdb-highly-underrrated">LMDB: Highly Underrrated&lt;/h4>
&lt;p>LMDB has some mindshare overlap with storage engines like LevelDB and RocksDB, but does not have the implicit clout of being born within Google or Facebook, despite its superior B-Tree design for read-heavy applications. The &lt;a href="http://www.lmdb.tech/doc/">API and documentation&lt;/a> unapologetically assume you know what you are doing, and have a deep understanding of trade-offs for your problem space, such as mandatory sorted insertion order for fast write performance with &lt;code>MDB_APPEND&lt;/code>. Its &lt;code>mmap&lt;/code>-based, zero-copy design precludes compression, for example; for Protomaps, random I/O latency is much more critical to the product than space savings, and disks are cheap relative to other resources.&lt;/p>
&lt;ul>
&lt;li>
&lt;p>You can watch my talk on &lt;a href="https://2019.stateofthemap.us/program/sun/osm-express-a-spatial-file-format-for-the-planet.html">OSM Express at State of the Map&lt;/a> and check out the &lt;a href="https://github.com/protomaps/OSMExpress">source code on GitHub.&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Mozilla&amp;rsquo;s &lt;a href="https://mozilla.github.io/firefox-browser-architecture/text/0015-rkv.html">Design Review: Key/Value Storage&lt;/a> explores storage engines including LMDB.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>LMDB author Howard Chu&amp;rsquo;s fiddle performance and technical deep dive at &lt;a href="https://www.youtube.com/watch?v=tEa5sAh-kVk">CMU Databaseology&lt;/a>.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="go">Go&lt;/h2>
&lt;p>Protomaps uses Go extensively as a &lt;em>complement&lt;/em> to C++.&lt;/p>
&lt;p>This is surprising to many developers who perceive Go as an &lt;em>alternative&lt;/em> to C++. Go doesn&amp;rsquo;t have many mature low-level libraries for computational geometry. Whether libraries could even be competitive with C++ is an open question, given Go&amp;rsquo;s memory model and approach to generic programming.&lt;/p>
&lt;p>Networking, HTTP and concurrency are solved problems in C++ via libraries like &lt;a href="https://think-async.com/Asio/">libasio&lt;/a> but anything but easy to implement, especially as a solo developer. These are all trifling matters in Go to the point where they are first-class parts of the standard library. Go programs inside Protomaps avoid external libraries whenever possible and average about 100 lines of code. A typical example is to listen for HTTP requests, unmarshal JSON in the request body, and &lt;code>fork/exec&lt;/code> a C++ program in a goroutine, reporting progress through a channel.&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/protomaps/PMTiles">go-pmtiles&lt;/a> is a library to read, write and serve the &lt;a href="https://github.com/protomaps/PMTiles">PMTiles&lt;/a> serverless tile archive format.&lt;/li>
&lt;/ul>
&lt;h2 id="other-likes">Other Likes&lt;/h2>
&lt;p>The Protomaps web application uses &lt;a href="https://www.djangoproject.com">Django&lt;/a>, mostly for its batteries-included auth and administration modules, and good-enough server rendered HTML. Scripting and lightweight geoprocessing is in Python, via the excellent &lt;a href="https://shapely.readthedocs.io/en/stable/manual.html">Shapely&lt;/a> and &lt;a href="https://rasterio.readthedocs.io/en/latest/">Rasterio&lt;/a> libraries. SQLite stores relational data like users and metrics. &lt;a href="https://github.com/evanw/esbuild">esbuild&lt;/a> makes all TypeScript development a breeze.&lt;/p>
&lt;h2 id="docker-nope">Docker: Nope&lt;/h2>
&lt;div style="text-align: center">
 &lt;img src="whale.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;em>source: &lt;a href="https://www.flickr.com/photos/67769030@N07/9839910925">flickr&lt;/a>&lt;/em>&lt;/p>
&lt;p>Given the breadth of software needed to make maps happen, it is surprising to developers that I &lt;em>consciously avoid&lt;/em> Docker and its ecosystem.&lt;/p>
&lt;p>Docker is a container format and runtime. It is incredibly useful if you are building Heroku or dotCloud, since building a Platform-as-a-Service (PaaS) demands multi-tenant isolation. Protomaps is not building a PaaS, nor are most companies. Companies often use Docker as a glorified static linker, and in the process introduce redundant abstractions and &lt;a href="https://en.wikipedia.org/wiki/There_are_known_knowns">unknown-unknown&lt;/a> failure modes.&lt;/p>
&lt;p>Docker for many in-house use cases solves a &lt;em>cultural problem rather than a technical one&lt;/em>: the historic separation of software development and operations, where &amp;ldquo;code&amp;rdquo; is tossed over the wall for SREs to deploy and carry pagers for.&lt;/p>
&lt;p>As a one-man shop doing both &lt;em>software development&lt;/em> and &lt;em>operations&lt;/em>, I can decree &lt;em>a priori&lt;/em> that all programs target a specific Ubuntu, depend on a set of packages from the OS, and run with a unprivileged user and designated port via a systemd configuration. My day-to-day development is still on macOS, but this is treated as a environment secondary to the production one.&lt;/p>
&lt;h4 id="revenge-of-docker">Revenge of Docker&lt;/h4>
&lt;p>It&amp;rsquo;s likely that a PaaS that Protomaps adopts is built on Docker, and might require some Docker incantations. It&amp;rsquo;s also possible that software Protomaps distributes to customers will be packaged via Docker, although statically linked, architecture-specific binaries are a better solution (&lt;a href="https://goreleaser.com">goreleaser&lt;/a> is my current choice for this).&lt;/p>
&lt;h2 id="postgresql-nope">PostgreSQL: Nope&lt;/h2>
&lt;div style="text-align: center">
 &lt;img src="elephant.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;em>source: &lt;a href="https://www.flickr.com/photos/67769030@N07/9839910925">flickr&lt;/a>&lt;/em>&lt;/p>
&lt;p>Protomaps does not use PostgreSQL, despite it being the de-facto standard for storing and querying GIS data via the excellent &lt;a href="https://postgis.net">PostGIS extension&lt;/a>.&lt;/p>
&lt;p>That is not to say PostgreSQL or PostGIS are flawed; they&amp;rsquo;re an exemplar of what open source geospatial tech can be and have had a massive impact on GIS in government. They are the perfect solution for relational geodata and multi-writer transaction processing over a network, neither of which are relevant to Protomaps.&lt;/p>
&lt;p>Many companies use PostGIS as a convenient SQL frontend to its underlying libraries, &lt;a href="https://gdal.org">GDAL/OGR&lt;/a> and &lt;a href="https://trac.osgeo.org/geos">GEOS&lt;/a>, when the problem could be solved in a simpler fashion by using those directly. By introducing a client-server database, automation becomes much more complex, involving ports, user authorization and process managers, instead of being self-contained in a single file like SQLite or LMDB. Protomaps&amp;rsquo; problem of scale again means that fetching data over the wire is just too much overhead, and efficient data structures for cartographic generalization, like &lt;a href="https://en.wikipedia.org/wiki/Planar_straight-line_graph">planar straight-line graphs&lt;/a>, cannot be hammered into the relational model.&lt;/p>
&lt;p>The pervasiveness of relational databases like PostGIS as a solution to all geo-shaped problems is, again, a cultural artifact. In this case, it&amp;rsquo;s the status quo of GIS as an academic discipline and industry: one dominated by a &lt;a href="https://joemorrison.medium.com/esri-cant-be-stopped-3b063f3de0b4">single company (Esri)&lt;/a> and its workflows and assumptions, where &amp;ldquo;doing GIS&amp;rdquo; means manipulating a relational database, and software development is abstracted away to a vendor.&lt;/p>
&lt;h2 id="other-nopes">Other Nopes&lt;/h2>
&lt;p>Despite years wrangling &lt;a href="https://rubyonrails.org">Ruby on Rails&lt;/a> in my Pivotal Labs days, I usually reach for Django for its standardized auth and admin. Server-side JavaScript is avoided; this means re-implementing logic in both JS and Go/Python, which I don&amp;rsquo;t mind. Java has a mature geospatial ecosystem with libraries like &lt;a href="https://github.com/locationtech/jts">JTS&lt;/a> and &lt;a href="https://wiki.openstreetmap.org/wiki/Osmosis">Osmosis&lt;/a>, but moving onto Java is all-or-nothing, given the packging conventions and JVM cold start times.&lt;/p>
&lt;h2 id="what-next">What Next?&lt;/h2>
&lt;p>The emerging projects I&amp;rsquo;m most excited about are &lt;a href="https://www.ogc.org/blog/4607">&amp;ldquo;cloud-optimized&amp;rdquo; formats for geodata&lt;/a>, including &lt;a href="https://flatgeobuf.org/">FlatGeobuf&lt;/a>, &lt;a href="https://github.com/sasakiassociates/geo-png-db">GeoPngDB&lt;/a>, and &lt;a href="https://copc.io">COPC&lt;/a> as complements to the format I&amp;rsquo;m developing, &lt;a href="https://github.com/protomaps/PMTiles">PMTiles&lt;/a>.&lt;/p>
&lt;p>If you&amp;rsquo;d like to keep up with Protomaps, or are interested in building and deploying custom, interactive maps of the entire world, you can reach me at &lt;a href="mailto:brandon@protomaps.com">brandon@protomaps.com&lt;/a> or find me on &lt;a href="http://bsky.app/profile/bdon.org">Bluesky&lt;/a> and &lt;a href="https://mastodon.social/@bdon">Mastodon&lt;/a>.&lt;/p></description></item><item><title>Dynamic Maps, Static Storage</title><link>https://protomaps.com/blog/dynamic-maps-static-storage/</link><pubDate>Thu, 06 May 2021 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/dynamic-maps-static-storage/</guid><description>&lt;p>&lt;strong>Update: This post describes the internals of the PMTiles v2 format. Check out the post on &lt;a href="https://protomaps.com/blog/blog/pmtiles-v3-whats-new/">PMTiles V3&lt;/a> for the latest!&lt;/strong>&lt;/p>
&lt;hr>
&lt;p>&lt;em>Serverless computing&lt;/em> supposedly will let developers run sophisticated applications on the web without wrangling virtual machines or databases. New products like Lambda offer simpler deploys for code; emerging standards like WebAssembly let you &lt;a href="https://phiresky.github.io/blog/2021/hosting-sqlite-databases-on-github-pages/">host SQLite databases on GitHub pages&lt;/a> and make queries all from your browser. These examples rely on developers adopting new workflows or programming environments. So what if I told you that you&amp;rsquo;ve been &lt;strong>computing serverlessly without realizing it?&lt;/strong>&lt;/p>
&lt;p>Behold this one megabyte embedded video, a clip from &lt;a href="https://www.youtube.com/watch?v=0fKBhvDjuy0">Powers of Ten: A Film Dealing with the Relative Size of Things in the Universe and the Effect of Adding Another Zero&lt;/a>:&lt;/p>
&lt;video controls preload="auto" width="100%" playsinline class="html-video">
 &lt;source src="https://protomaps.com/blog/blog/dynamic-maps-static-storage/PowersOfTen.mp4" type="video/mp4">
 &lt;span>Your browser doesn't support embedded videos, but don't worry, you can &lt;a href="https://protomaps.com/blog/blog/dynamic-maps-static-storage/PowersOfTen.mp4">download it&lt;/a> and watch it with your favorite video player!&lt;/span>
&lt;/video>
&lt;p>There&amp;rsquo;s a couple things going on here:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>You can seek to timestamps using the embedded controls to see the Chicago lakefront at different scales; consider this an act of &lt;em>querying a read-only database via an index.&lt;/em>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The video is a single file, hostable anywhere, and playable by all modern browsers.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>If you open your browser&amp;rsquo;s development tools, you&amp;rsquo;ll see requests for specific byte offsets in the file with &lt;em>Range:&lt;/em> headers.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>This simple example highlights the &lt;strong>ease of use of video&lt;/strong>; A video is thought of as a single unit of content, like a text file or photograph, and not a kind of trendy &lt;em>serverless tech&lt;/em>.&lt;/p>
&lt;h2 id="codecs-make-video-easy">Codecs make Video Easy&lt;/h2>
&lt;p>A remarkable aspect of the above example is that it&amp;rsquo;s a single megabyte, despite being made of hundreds of individual frames. Assuming each frame is a 200 kilobyte still, storing the eight second video as a directory of images would take &lt;em>fifty times the space!&lt;/em>&lt;/p>
&lt;p>We can expose the secrets of video codecs using ffmpeg:&lt;/p>
&lt;pre tabindex="0">&lt;code>ffmpeg -flags2 +export_mvs -i in.mp4 -vf codecview=mv=pf+bf+bb out.mp4
&lt;/code>&lt;/pre>&lt;div style="text-align: center">
 &lt;img src="motion_vectors.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>White arrows depict motion vectors for blocks of pixels: the codec &lt;strong>exploits redundancy across data to efficiently encode a single archive.&lt;/strong> This is possible because most videos are not random noise; the &lt;a href="https://adydychk.medium.com/what-is-a-video-codec-and-how-does-it-work-6f90bd037fa0">contents of a single frame are highly predictive of the next frames.&lt;/a>&lt;/p>
&lt;h2 id="a-map-is-just-a-video">A Map is just a Video&lt;/h2>
&lt;p>At first blush, a dynamic map on the web isn&amp;rsquo;t much like a video and more like a web service; something database-shaped and probably server-ish, despite being a read-only experience. The &lt;a href="https://www.ogc.org/standards/wmts">Web Map Tile Service&lt;/a> standard defines a HTTP server API for serving up maps; map tiles on &lt;a href="https://openstreetmap.org">openstreetmap.org&lt;/a> are accessed by Web Mercator Z/X/Y URLs.&lt;/p>
&lt;p>Given the above encoding example though, let&amp;rsquo;s imagine if &lt;strong>dynamic maps, like videos, were just archives of images, but with frames that traverse space instead of time.&lt;/strong>&lt;/p>
&lt;p>Armed with this insight, we can invent a &lt;em>codec for map data&lt;/em> with these goals:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>It should be a single file.&lt;/strong> There are existing workflows to host maps on S3 by syncing individual tiles and directories, but this is inconvenient in the same way syncing tens of thousands of still video frames is inefficient.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>It should work the same way for all scales&lt;/strong>, from a small area map of the Chicago lakefront to the entire Earth.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>It should be as simple as possible for the Z/X/Y use case, and hold arbitrary raster or vector tiles&lt;/strong>, which rules out backwards-compatible formats like &lt;a href="https://www.cogeo.org">Cloud Optimized GeoTIFFs&lt;/a>.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>A necessary step in making this efficient is, like video encoding, to &lt;strong>exploit redundancy across data.&lt;/strong> Any global-scale map system must &lt;em>acknowledge the earth is 70% ocean.&lt;/em> Map image APIs already take advantage of this by &lt;a href="https://www.mjt.me.uk/posts/smallest-png/">special-casing solid blue PNG images.&lt;/a>&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="smallest256.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;h2 id="pmtiles-pyramids-of-map-tiles">PMTiles: Pyramids of Map Tiles&lt;/h2>
&lt;p>&lt;a href="https://github.com/protomaps/PMTiles">PMTiles&lt;/a> is a single-file archive format for pyramids of map tiles that &lt;strong>aims to be the simplest possible implementation&lt;/strong> for the above requirements. It has a &lt;a href="https://github.com/protomaps/PMTiles">short and sweet specification&lt;/a> that I&amp;rsquo;ve made available in the public domain, along with BSD-licensed Python and JavaScript reference implementations.&lt;/p>
&lt;p>Here&amp;rsquo;s a visual for how a simple PMTiles archive is organized:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="pmtiles_explainer_1.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The process for fetching one tile like &lt;code>z:8 x:65 y:95&lt;/code>:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Fetch the first 512 kilobytes and parse the directory into a lookup table&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Look up by key of the tile you want, in this case &lt;code>8_65_95&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>It&amp;rsquo;s a match! You&amp;rsquo;ll have a byte range &lt;code>offset: 785366 length: 21400&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Fetch those bytes using &lt;code>Range:bytes=785366-806765&lt;/code> and interpret the data as an image&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>Here&amp;rsquo;s an interactive example using the &lt;a href="https://github.com/protomaps/PMTiles/blob/master/js/index.mjs">sub-200-line pmtiles.js decoder library&lt;/a> in the browser, with &lt;a href="https://s2maps.eu">Creative Commons satellite imagery from EOX&amp;rsquo;s S2 Cloudless product&lt;/a>, containing some tiles covering our happy picnic on the Chicago lakefront:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">pmtiles&lt;/span>.&lt;span style="color:#a6e22e">getZxy&lt;/span>(&lt;span style="color:#a6e22e">coord&lt;/span>.&lt;span style="color:#a6e22e">z&lt;/span>,&lt;span style="color:#a6e22e">coord&lt;/span>.&lt;span style="color:#a6e22e">x&lt;/span>,&lt;span style="color:#a6e22e">coord&lt;/span>.&lt;span style="color:#a6e22e">y&lt;/span>).&lt;span style="color:#a6e22e">then&lt;/span>(&lt;span style="color:#a6e22e">result&lt;/span> =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> &lt;span style="color:#a6e22e">range&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;bytes=&amp;#39;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#a6e22e">result&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#e6db74">&amp;#39;-&amp;#39;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> (&lt;span style="color:#a6e22e">result&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>]&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#a6e22e">result&lt;/span>[&lt;span style="color:#ae81ff">1&lt;/span>]&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fetch&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;PowersOfTwo.pmtiles&amp;#39;&lt;/span>,{&lt;span style="color:#a6e22e">headers&lt;/span>&lt;span style="color:#f92672">:&lt;/span>{&lt;span style="color:#a6e22e">Range&lt;/span>&lt;span style="color:#f92672">:&lt;/span>&lt;span style="color:#a6e22e">range&lt;/span>}).&lt;span style="color:#a6e22e">then&lt;/span>(&lt;span style="color:#a6e22e">resp&lt;/span> =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">resp&lt;/span>.&lt;span style="color:#a6e22e">arrayBuffer&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }).&lt;span style="color:#a6e22e">then&lt;/span>(&lt;span style="color:#a6e22e">buf&lt;/span> =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">blob&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">Blob&lt;/span>( [&lt;span style="color:#a6e22e">buf&lt;/span>], { &lt;span style="color:#a6e22e">type&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#e6db74">&amp;#34;image/jpg&amp;#34;&lt;/span> } )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">imageUrl&lt;/span> &lt;span style="color:#f92672">=&lt;/span> window.&lt;span style="color:#a6e22e">URL&lt;/span>.&lt;span style="color:#a6e22e">createObjectURL&lt;/span>(&lt;span style="color:#a6e22e">blob&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">img&lt;/span>.&lt;span style="color:#a6e22e">current&lt;/span>.&lt;span style="color:#a6e22e">src&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">imageUrl&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The initial fetch for the first 512 kilobytes is performed once and cached in the browser, so subsequent requests don&amp;rsquo;t incur the same overhead as the first tile. This again exploits the common panning+zooming access pattern, like what&amp;rsquo;s happening in this integration with &lt;a href="https://leafletjs.org">Leaflet&lt;/a>:&lt;/p>
&lt;p>A consequence of the above design is that &lt;strong>multiple entries can point to the same offset.&lt;/strong> A map of an island surrounded by lots of ocean will store our solid blue image once, with any number of tile entries referencing it. Multiple requests for this same offset will return our Blue Friend cached in the browser:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="pmtiles_explainer_2.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;h2 id="adding-more-zeros">Adding more Zeros&lt;/h2>
&lt;p>The above example has a clear limitation, which is that the blind initial fetch is for only 512 kilobytes. A reasonable planet-scale map can have fifteen zoom levels; the sum count of tiles then is&lt;/p>
&lt;p>1 + 4 + 16 + 64 + 256 + 1,024 + 4,096 + 16,384 + 65,536 + 262,144 + 1,048,576 + 4,194,304 + 1,6777,216 + 67,108,864 + 268,435,456&lt;/p>
&lt;p>or &lt;strong>357,913,941 tiles.&lt;/strong> Given that PMTiles needs 17 bytes per entry, a full directory pyramid with 15 zoom levels is &lt;strong>six gigabytes&lt;/strong> - impractical for any browser to fetch at once, especially if your map will only ever display a couple tiles!&lt;/p>
&lt;p>We can solve this by adding a layer of indirection via &lt;strong>leaf directories.&lt;/strong> A single directory in PMTiles has a hard cap of 21,845 entries, or a complete pyramid for eight zoom levels. At the maximum level of the root directory, the top bit of Z is flipped to describe the bytes at the referenced offset being not tile data but &lt;em>another directory with up to 21,845 entries.&lt;/em>&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="pmtiles_explainer_3.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>Example of how looking up &lt;code>z:14 x:4204 y:6090&lt;/code> works with leaf directories:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Fetch the first 512 kilobytes, parse the root directory.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Check the root directory for &lt;code>14_4204_6090&lt;/code>. It doesn&amp;rsquo;t exist, because the root directory only contains levels 0-7.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Check the root directory for a &lt;em>leaf directory entry&lt;/em> of the parent tile. The z7 parent tile of &lt;code>z:14 x:4204 y:6090&lt;/code> is &lt;code>z:7 x:32 y:47&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Fetch the bytes for the leaf directory and parse it into a lookup table.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Index into the leaf directory with &lt;code>14_4204_6090&lt;/code> for the byte offset and length of the tile data.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>As long as clients cache recently used lookup tables, and zooming/panning remains within &lt;code>z:7 x:32 y:47&lt;/code>, &lt;strong>no directory fetches are required after the first tile.&lt;/strong> The ideal ordering of tile data and leaf directories may be on &lt;a href="https://en.wikipedia.org/wiki/Hilbert_curve">space-filling curves&lt;/a> to minimize hardware page faults, but this is an optimization and outside the spec.&lt;/p>
&lt;hr>
&lt;p>An astute reader will notice another seeming flaw in the above design: &lt;strong>what if your pyramid is exactly a full nine levels?&lt;/strong> In this case, the root directory has a full 21,845 entries, and points to 16,384 leaf directories, each containing only five (one Z7 + four Z8) entries! Panning the map at Z7 would then incur a directory fetch for every single tile.&lt;/p>
&lt;p>&lt;strong>Inefficiently packed leaf directories can be solved the exact same way as duplicated ocean tiles.&lt;/strong> One leaf directory should contain multiple sibling sub-pyramids, up to 21,845 entries in total; the root directory then holds multiple leaf pointers to the same offset. As long as clients cache directories by offset and not ZXY, panning at Z7 will cost few extra fetches.&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="pmtiles_explainer_4.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;hr>
&lt;p>Here&amp;rsquo;s the limits implied by the 17 byte keys in PMTiles:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;em>3 bytes for X and Y:&lt;/em> maximum zoom of 23&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;em>6 bytes for offsets:&lt;/em> maximum archive size of 281 terabytes&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;em>4 bytes for lengths:&lt;/em> maximum individual tile size of 4.3 gigabytes&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="go-pmtiles-a-pmtiles-proxy">go-pmtiles: a PMTiles proxy&lt;/h2>
&lt;p>If you&amp;rsquo;re thinking, &lt;em>Yes Brandon, very clever design! We want to use this but all of our existing map systems read Z/X/Y tiles from a server&amp;hellip;&lt;/em> I&amp;rsquo;ve written a &lt;a href="https://github.com/protomaps/go-pmtiles">caching proxy&lt;/a> that decodes your PMTiles on S3 via a traditional Z/X/Y endpoint. While no longer purely serverless, this is a two-megabyte executable that serves an entire bucket of PMTiles on a micro instance like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># to serve my-bucket/EXAMPLE_1.pmtiles and my-bucket/EXAMPLE-2.pmtiles...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>go-pmtiles https://my-bucket.s3-us-east-1.amazonaws.com
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;gt; Serving https://my-bucket.s3-us-east-1.amazonaws.com on HTTP port: &lt;span style="color:#ae81ff">8077&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># fetch the tiles...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl http://localhost:8077/EXAMPLE_1/0/0/0.jpg
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl http://localhost:8077/EXAMPLE_2/1/0/0.jpg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The details:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Inherits all the simplicity of PMTiles: a short, concurrent, evented Go program with zero runtime dependencies&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Caches directories on the server to minimize latency on the client&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Implements GZIP compression for uncompressed formats like vector Protobufs, since byte serving and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding">Content-Encoding&lt;/a> don&amp;rsquo;t play nicely together&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>You can download binary releases and source code at &lt;a href="https://github.com/protomaps/go-pmtiles">github.com/protomaps/go-pmtiles.&lt;/a>&lt;/p>
&lt;h2 id="more-stuff">More Stuff&lt;/h2>
&lt;p>PMTiles is just one small part of &lt;a href="https://protomaps.com">Protomaps&lt;/a>, a one-man project to rethink web cartography, and reflects many of the system&amp;rsquo;s core principles:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Less computation:&lt;/strong> &amp;ldquo;Serverless&amp;rdquo; patterns are here to stay: vendors achieve economies of scale; developers pay less; mapmaking projects outside of dedicated technology firms — by museums, cultural institutions, or journalists — can be preserved across time.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Dependency-free:&lt;/strong> Favor minimalist formats and statically-linked binaries over more features or backwards compatibility.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Scale-free:&lt;/strong> Tools and process knowledge for making an &lt;a href="https://millsfield.sfomuseum.org/blog/2021/05/03/geotagging/">airport-sized map&lt;/a> work the exact same way for a city, a country, and the entire Earth with no jumps in complexity.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>If this system sounds interesting to you, you can reach me at &lt;a href="mailto:brandon@protomaps.com">brandon@protomaps.com&lt;/a> or find me on &lt;a href="http://bsky.app/profile/bdon.org">Bluesky&lt;/a> and &lt;a href="https://mastodon.social/@bdon">Mastodon&lt;/a>.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://github.com/mapbox/mbtiles-spec">MBTiles&lt;/a> can deduplicate tiles via views, but must be accessed as a SQLite database.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://www.w3.org/2020/maps/">W3C/OGC Joint Workshop Series on Maps for the Web&lt;/a> addresses &lt;em>&amp;ldquo;Adding a native map viewer for the Web platform, similar to how HTML video was added for video content.&amp;rdquo;&lt;/em>&lt;/p>
&lt;/li>
&lt;/ul></description></item><item><title>A new way to make maps with OpenStreetMap</title><link>https://protomaps.com/blog/new-way-to-make-maps/</link><pubDate>Thu, 22 Apr 2021 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/new-way-to-make-maps/</guid><description>&lt;p>If you develop for the web, you&amp;rsquo;ve probably heard of &lt;a href="https://openstreetmap.org">OpenStreetMap&lt;/a>, an open data alternative to Google Maps. Despite the wide &lt;em>name recognition&lt;/em> of OSM, you may have questions like &lt;em>how do I make a web map with OSM data?&lt;/em> and &lt;em>how can I host maps myself?&lt;/em>&lt;/p>
&lt;p>There are popular &lt;a href="https://switch2osm.org">guides&lt;/a> on how to work with planet files, PostGIS databases, and run tileservers for the &lt;a href="https://openstreetmap.org">OSM &amp;ldquo;slippy map&amp;rdquo; style&lt;/a>. Even if you figure those out, you may be curious about &lt;strong>vector maps&lt;/strong> for high-DPI sharpness and client-side customization of appearance and labels.&lt;/p>
&lt;p>&lt;strong>Protomaps&lt;/strong> is a new basemap system which is an end-to-end rethinking of this entire stack, oriented around the idea that &lt;strong>custom mapmaking should be simple.&lt;/strong>&lt;/p>
&lt;p>In about five minutes, you can select any area in the world and get a self-contained map that runs locally, offline or serverlessly on S3 - check out the &lt;a href="https://protomaps.com/blog/docs/pmtiles">Getting Started&lt;/a> guide.&lt;/p>
&lt;h2 id="building-blocks">Building Blocks&lt;/h2>
&lt;p>Web maps are a &amp;ldquo;full-stack&amp;rdquo; challenge: Aesthetic goals, such as a sensible arrangement of map labels, need to be supported not only by JavaScript rendering but also the underlying data backend. Protomaps is designed as an end-to-end system for this reason — see &lt;a href="https://protomaps.com/blog/docs/">What is Protomaps?&lt;/a> — although each part is interoperable with other tools.&lt;/p>
&lt;p>As of now (April 2021), each component is viable and being used in production; I&amp;rsquo;ll describe how each &lt;em>fills a specific gap in the map ecosystem, as well as the direction I am developing it towards.&lt;/em>&lt;/p>
&lt;h3 id="protomapsjs-a-vector-map-renderer-and-labeler-for-the-web">protomaps.js: a vector map renderer and labeler for the web&lt;/h3>
&lt;div style="text-align: center">
 &lt;img src="editor_preview.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;a href="https://github.com/protomaps/protomaps.js">protomaps.js on GitHub&lt;/a>&lt;/p>
&lt;h4 id="where-it-fits">Where it fits&lt;/h4>
&lt;ul>
&lt;li>
&lt;p>There&amp;rsquo;s powerful WebGL-based map renderers like &lt;a href="https://github.com/mapbox/mapbox-gl-js">MapboxGL JS&lt;/a>, &lt;a href="https://github.com/tangrams/tangram">Tangram&lt;/a> and &lt;a href="https://www.harp.gl">harp.gl&lt;/a> for vector map tiles. Despite this, the &lt;a href="https://leafletjs.com">Leaflet&lt;/a> library remains extremely popular because of its simple, DOM-driven design. Protomaps.js brings &lt;strong>high-DPI vector rendering and map customization to the Leaflet map experience,&lt;/strong> with some deliberate compromises: see &lt;a href="https://protomaps.com/blog/docs/frontends/leaflet">what protomaps.js is not&lt;/a>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Protomaps.js is also a system for &lt;strong>code-driven map symbology&lt;/strong>. While the above WebGL libraries and image-based renderers like &lt;a href="https://mapnik.org">Mapnik&lt;/a> expose a limited API for map labels and markers, protomaps.js is &lt;strong>extensible at runtime by implementing custom Symbolizers&lt;/strong>; it&amp;rsquo;s an &lt;a href="https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics)">immediate-mode&lt;/a> system for direct access to Canvas.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h4 id="whats-next">What&amp;rsquo;s next&lt;/h4>
&lt;ul>
&lt;li>
&lt;p>protomaps.js is alpha stage and needs a lot of work — it&amp;rsquo;s being used to power all the maps on this site, but the internal API is still unstable. &lt;em>I am open sourcing it now to gauge interest in a pure 2D webmap renderer&lt;/em>: &lt;a href="https://github.com/protomaps/protomaps.js">protomaps.js on GitHub&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Labels are currently, well, bad. Linear feature labels are mostly broken; multiple candidate positions should be considered for point labels like cities and POIs. Some features, such as country-specific highway shields, will need to be supported by the vector data from the Downloads service and web map API.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Support SVG icons via the &lt;a href="https://github.com/protomaps/protosprites">protosprites library&lt;/a> and port popular open source map styles such as &lt;a href="http://maps.stamen.com">Stamen Design&amp;rsquo;s Toner&lt;/a>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>All features need to be designed with &lt;strong>performance&lt;/strong> as a primary goal: while a pure Canvas-based experience will never be as slick as a WebGL one, protomaps.js aims to be just as good as &amp;ldquo;slippy&amp;rdquo; image maps.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="pmtiles-a-serverless-format-for-map-tiles">PMTiles: a serverless format for map tiles&lt;/h3>
&lt;div style="text-align: center">
 &lt;img src="pmtiles_layout.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;a href="https://github.com/protomaps/PMTiles">Spec and Reference Python/JS implementations on GitHub&lt;/a>&lt;/p>
&lt;h4 id="where-it-fits-1">Where it fits&lt;/h4>
&lt;ul>
&lt;li>
&lt;p>Vector map data in the form of Z/X/Y tiles is typically made available as a commercial API. If you don&amp;rsquo;t need the entire planet, it should be possible to host a piece of the world yourself.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Existing formats like &lt;a href="https://github.com/mapbox/mbtiles-spec">MBTiles&lt;/a> are based on SQLite. This &lt;strong>limits the ability to self-host maps&lt;/strong> to those confident running a server in production.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>PMTiles is a format based on HTTP Byte Serving that can be &lt;strong>hosted on S3&lt;/strong> or any other cloud provider. It can be read directly by the protomaps.js renderer for a totally serverless map application. It&amp;rsquo;s a perfect match for projects where a long-term subscription to a hosted service is undesirable.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h4 id="whats-next-1">What&amp;rsquo;s next&lt;/h4>
&lt;ul>
&lt;li>
&lt;p>Try the &lt;a href="https://github.com/protomaps/pmtiles">PMTiles specification and reference implementations&lt;/a> for your own projects.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>More implementations of decoders. In the works: a concurrent, caching decoder for Go.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="map-downloads--web-api">Map Downloads &amp;amp; Web API&lt;/h3>
&lt;div style="text-align: center">
 &lt;img src="BundleStep1.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;a href="https://protomaps.com/downloads/small_map">Downloads&lt;/a> | &lt;a href="https://protomaps.com/blog/docs">Web API Documentation&lt;/a>&lt;/p>
&lt;h4 id="where-it-fits-2">Where it fits&lt;/h4>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Eliminates all complexity from getting map data:&lt;/strong> The current status quo is to use PostgreSQL as an intermediate store for OSM data, and to render tiles from PostgreSQL on-demand. Instead, download pre-rendered PMTiles from the &lt;a href="https://protomaps.com/downloads">Downloads&lt;/a> site, or use the &lt;a href="https://protomaps.com/blog/docs">global Web API&lt;/a>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Map Downloads (but not web API) snapshot OSM every 60 seconds: this turns OpenStreetMap into a &lt;strong>truly live, editable project&lt;/strong>. This resolves the mismatch between the idea that the map can be improved by anyone, and the reality that map services are weeks or months behind the live data.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>For those needing a global hosted service, you can &lt;a href="https://protomaps.com/dashboard">sign up for an API key&lt;/a>.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h4 id="whats-next-2">What&amp;rsquo;s next&lt;/h4>
&lt;ul>
&lt;li>
&lt;p>Scaling up: Map downloads are currently limited to 500,000 nodes to help isolate bugs - for example, sometimes the ocean &amp;ldquo;leaks&amp;rdquo; into land. If you find problems or have questions, please &lt;a href="#whats-next-for-protomaps">email me&lt;/a>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Map downloads and the Web API should support high-quality label generalizations, highway shields, and beautiful graph-based simplification of road networks.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>The Infinite Map:&lt;/strong> PMTiles downloads should be customizable to include any object or tag in OpenStreetMap, via the OSM Express database.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>OSM as a live project also comes with risks&lt;/strong> related to vandalism or just broken data. Monthly releases of &lt;a href="http://daylightmap.org">Facebook&amp;rsquo;s Daylight Map Distribution&lt;/a> should be an option for Map Downloads and the Web API.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="osm-express-a-database-for-openstreetmap">OSM Express: a database for OpenStreetMap&lt;/h3>
&lt;div style="text-align: center">
 &lt;img src="https://github.com/protomaps/OSMExpress/blob/main/examples/screenshot.png?raw=true" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>&lt;a href="https://github.com/protomaps/OSMExpress">Documentation&lt;/a> | &lt;a href="https://protomaps.com/downloads/osm">Minutely Extracts&lt;/a>&lt;/p>
&lt;h4 id="where-it-fits-3">Where it fits&lt;/h4>
&lt;ul>
&lt;li>
&lt;p>Relational databases are the most popular way to work with OSM, but demand a lossy transformation of the data model. OSMX is a embedded database for nodes, ways and relations to make building applications on fresh OSM data simple. &lt;a href="https://github.com/protomaps/OSMExpress">Check it out on GitHub.&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>All Protomaps services are built on top of OSMX; it&amp;rsquo;s orders of magnitude faster than alternatives. It&amp;rsquo;s being used by other companies for tasks like &lt;a href="https://github.com/azavea/onramp">generating diffs&lt;/a>. &lt;a href="https://protomaps.extracts">Minutely Extracts&lt;/a> provides fresh PBF format data for use in other applications like routing and geocoding.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h4 id="whats-next-3">What&amp;rsquo;s next&lt;/h4>
&lt;ul>
&lt;li>
&lt;p>More OSMX reader libraries in other languages.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Applications for visualizing OSM activity in near-realtime built on OSMX.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="whats-next-for-protomaps">What&amp;rsquo;s next for Protomaps&lt;/h2>
&lt;p>Aside from the existing parts above, there&amp;rsquo;s other features on the roadmap:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Terrain&lt;/strong>: protomaps.js should render client-side hillshades and contour lines, and &lt;a href="https://github.com/mapzen/terrarium">Terrarium-format&lt;/a> tiles should be downloadable as PMTiles archives, based on SRTM, ASTER or a mosaic dataset.&lt;/li>
&lt;li>&lt;strong>Satellite&lt;/strong>: A cloudless Landsat or Sentinel-2 imagery layer is a great match for canvas-rendered, custom map labels.&lt;/li>
&lt;/ul>
&lt;p>I want to end by clarifying that Protomaps is &lt;strong>a self-funded, single-person product, executing on a distinct vision of what web cartography can be.&lt;/strong> If your company or project has a mapping challenge I might help you solve, email me at &lt;a href="mailto:brandon@protomaps.com">brandon@protomaps.com&lt;/a> or find me on &lt;a href="http://bsky.app/profile/bdon.org">Bluesky&lt;/a> and &lt;a href="https://mastodon.social/@bdon">Mastodon&lt;/a>, and I&amp;rsquo;ll of course be publishing on &lt;a href="https://github.com/bdon">GitHub&lt;/a>.&lt;/p>
&lt;p>You&amp;rsquo;ll see more on this blog soon!&lt;/p>
&lt;p>🗺️&lt;/p></description></item><item><title>Zonal Statistics for OpenStreetMap in the browser</title><link>https://protomaps.com/blog/osm-estimate/</link><pubDate>Mon, 16 Mar 2020 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/osm-estimate/</guid><description>&lt;p>A common user interaction in GIS applications is selecting a bounding box or polygon:&lt;/p>
&lt;p>Your application may then use this polygon to do things like:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Render a high resolution, printable map image.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Generate a report of the surface area of buildings.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Calculate an isochrone map of distance to transit.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;div style="text-align: center">
 &lt;img src="bbox_example.jpg" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>Users of your service expect that any action will complete in a reasonable amount of time. It&amp;rsquo;s smart to build in a limit for the amount of data processed on the server. One simple way to do this is an area limit, for example: 100 square kilometers.&lt;/p>
&lt;p>Using total area doesn&amp;rsquo;t work well for datasets of man-made objects like OpenStreetMap. The density of features has huge variance in different parts of the world. In the image above, the area on the left is only part of Paris and contains about 10 million nodes. The area on the right is most of the U.S. state of Minnesota, and also has about 10 million nodes. A system limit designed for the densest areas would be too restrictive.&lt;/p>
&lt;h3 id="density">Density&lt;/h3>
&lt;p>Instead of having an area limit, we can first test the area for the count of features, and then provide feedback in the UI if the limit is exceeded. Common ways to count are:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>For data in PostgreSQL, query over a spatial index using a function like &lt;!-- raw HTML omitted -->ST_Intersects, returning a row count.&lt;!-- raw HTML omitted -->&lt;/p>
&lt;/li>
&lt;li>
&lt;p>For OpenStreetMap, use &lt;!-- raw HTML omitted -->Overpass API&lt;!-- raw HTML omitted --> to count nodes or features matching a tag, returning a count output.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>This information can be exposed to a web browser via API.&lt;/p>
&lt;h3 id="faster-simpler">Faster, Simpler&lt;/h3>
&lt;p>The previous approach has the major drawback of needing a remote, blocking server call. We can instead design a quicker way to estimate the number of features without a dynamic query. This allows for near-instant feedback in the UI, and less moving parts, at the cost of being a coarse approximation.&lt;/p>
&lt;p>The &lt;!-- raw HTML omitted -->GeoTIFF format&lt;!-- raw HTML omitted --> is a natural fit for this problem. It&amp;rsquo;s readable by all GIS libraries, supports compression, and embeds a geographic projection.&lt;/p>
&lt;p>Each pixel in this image is one level 12 Web Mercator Tile, and each sample is a 16 bit unsigned OpenStreetMap node count (in thousands). This is too dark to be visible to the human eye, but normalizing the color range results in this image:&lt;/p>
&lt;div style="text-align: center">
 &lt;img src="osm_nodes.png" alt="" style="max-width: 100%;
 max-height: 300px; margin-left:auto; margin-right: auto">
&lt;/div>

&lt;p>The resulting compressed GeoTIFF is only 212 KB, which is reasonable to deliver to web browsers. The source code for generating this GeoTIFF is available at &lt;!-- raw HTML omitted -->github.com/protomaps/OSMEstimator&lt;!-- raw HTML omitted -->.&lt;/p>
&lt;h3 id="calculating-estimates">Calculating Estimates&lt;/h3>
&lt;p>Any zonal statistics library can be used to compute the sum total of an area of interest over the GeoTIFF&amp;rsquo;s samples. The &lt;!-- raw HTML omitted -->GeoBlaze&lt;!-- raw HTML omitted --> library implements this in JavaScript. The &lt;!-- raw HTML omitted -->Rasterio&lt;!-- raw HTML omitted --> library implements this in Python. Example of computing a sum in Python:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-py" data-lang="py">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> pyproj
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> rasterio &lt;span style="color:#f92672">import&lt;/span> mask
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> shapely.geometry &lt;span style="color:#f92672">import&lt;/span> shape
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RASTER &lt;span style="color:#f92672">=&lt;/span> rasterio&lt;span style="color:#f92672">.&lt;/span>open(&lt;span style="color:#e6db74">&amp;#39;osm_nodes.tif&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PROJECT &lt;span style="color:#f92672">=&lt;/span> partial(pyproj&lt;span style="color:#f92672">.&lt;/span>transform,pyproj&lt;span style="color:#f92672">.&lt;/span>Proj(init&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#39;epsg:4326&amp;#39;&lt;/span>),pyproj&lt;span style="color:#f92672">.&lt;/span>Proj(init&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#39;epsg:3857&amp;#39;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transformed &lt;span style="color:#f92672">=&lt;/span> transform(PROJECT,shape(geom)) &lt;span style="color:#75715e">#geom is a GeoJSON-like dict&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>masked &lt;span style="color:#f92672">=&lt;/span> mask&lt;span style="color:#f92672">.&lt;/span>mask(RASTER,[transformed],all_touched&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">False&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>estimate &lt;span style="color:#f92672">=&lt;/span> masked[&lt;span style="color:#ae81ff">0&lt;/span>]&lt;span style="color:#f92672">.&lt;/span>sum() &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">1000&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="limitations">Limitations&lt;/h3>
&lt;p>The choice of zoom level 12 and the accuracy of the estimation rely on query areas being significantly larger than the pixels of the GeoTIFF. Different applications may want to modify this code for:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Specific features: Modify &lt;!-- raw HTML omitted -->the program&lt;!-- raw HTML omitted --> to only consider ways tagged building=yes, instead of all OpenStreetMap nodes.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>If the relevant area is only part of the world, the GeoTIFF size can be reduced by cropping the raster.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Trade-offs between estimation accuracy and file size can be controlled by using 8, 16 or 32 bit samples.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="see-also">See Also&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://tyrasd.github.io/osm-node-density/#2/38.0/13.0/latest,places">OpenStreetMap Node Density&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://osm-analytics.org/#/">OSM Analytics&lt;/a>&lt;/p>
&lt;/li>
&lt;/ul></description></item><item><title/><link>https://protomaps.com/blog/posts/explore-overture-maps/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://protomaps.com/blog/posts/explore-overture-maps/</guid><description>&lt;h1 id="heading">&lt;/h1></description></item></channel></rss>