Parcels on a map in five steps.
No SDK, no build plugin, no account required to read this page. If you have written MapLibre before, steps 2 and 3 are the only unfamiliar part — and only barely.
Install the two packages
MapLibre renders the map; the PMTiles library teaches it how to read a single-file archive over HTTP range requests. There is nothing else to install.
npm install maplibre-gl pmtilesRegister the pmtiles:// protocol
This has to happen once, before any map is constructed. It teaches MapLibre to resolve pmtiles:// URLs by issuing range requests instead of fetching whole tiles.
import maplibregl from 'maplibre-gl'
import { Protocol } from 'pmtiles'
import 'maplibre-gl/dist/maplibre-gl.css'
const protocol = new Protocol()
maplibregl.addProtocol('pmtiles', protocol.tile)Create the map with a basemap source
The basemap is one planet-wide archive. Point at it the same way you would any vector source — the only difference is the pmtiles:// prefix on the URL.
const TILES = 'https://tiles.mapsfordevelopers.com'
const map = new maplibregl.Map({
container: 'map',
center: [-95.3698, 29.7604], // Houston, TX
zoom: 14,
style: {
version: 8,
glyphs: 'https://protomaps.github.io/basemaps-assets'
+ '/fonts/{fontstack}/{range}.pbf',
sources: {
basemap: {
type: 'vector',
url: `pmtiles://${TILES}/basemap/protomaps_planet.pmtiles`,
},
},
layers: [
{ id: 'bg', type: 'background',
paint: { 'background-color': '#0F141C' } },
],
},
})Add the parcel layer
Parcels are ordinary vector tiles, so every MapLibre paint property, filter expression and interaction handler works exactly as documented.
map.on('load', () => {
map.addSource('parcels', {
type: 'vector',
url: `pmtiles://${TILES}/parcels/parcels_tx_harris.pmtiles`,
})
map.addLayer({
id: 'parcel-fill',
type: 'fill',
source: 'parcels',
'source-layer': 'parcels',
paint: { 'fill-color': '#FF6B35', 'fill-opacity': 0.12 },
})
map.addLayer({
id: 'parcel-line',
type: 'line',
source: 'parcels',
'source-layer': 'parcels',
paint: { 'line-color': '#FF8A4C', 'line-width': 0.6 },
})
})Read the attributes on click
Whatever fields the county publishes come through on the feature properties — owner, assessed value, legal description, acreage. The exact set varies by source.
map.on('click', 'parcel-fill', (e) => {
const parcel = e.features?.[0]
if (!parcel) return
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(`<pre>${JSON.stringify(parcel.properties, null, 2)}</pre>`)
.addTo(map)
})
map.on('mouseenter', 'parcel-fill', () => {
map.getCanvas().style.cursor = 'pointer'
})Stuck on something?
The people who built the pipeline answer the contact form. Send the code that is not working.