Thoughts on code, digital cartography, and technology.

Featured Projects

All Projects →
Places Search
Places Search
webapp
duckdb
parquet
wasm

Search 5.2M GeoNames places in the browser using DuckDB-WASM and HTTP range requests over Parquet data.

TRMNL Plugins
TRMNL Plugins
fullstack
maplibre
node

A variety of custom plugins for the TRMNL e-ink display.

RSSignals
RSSignals
fullstack
maplibre
node
ollama
python
sqlite

An AI-curated daily brief of the day's news, distilled from RSS feeds with a bent to progresive and tech-focused outlets.

Mapping Disasters
Mapping Disasters
fullstack
maplibre
sqlite

A map visualization of open satellite Imagery from Maxar, Capella, and Umbra.

Currently Reading

All Books →

Journal

View all →

Places as Parquet: A Prefix and Free-Text Search that Runs in the Browser

· 1 min read

~2.5M cities · ~375 MB served on demand from Cloudflare R2 · sub-second prefix queries · zero backend

Journal | Read More →

Benchmarking Spatial Joins in DuckDB

· 4 min read

An aggregate FILTER made this DuckDB spatial query 84× faster than a plain WHERE, with the same exact count and no index or precomputation.

Journal | Read More →

US Forest Service OSM Mashup

· 2 min read

While scoping out my next backpacking adventure via various PDF and web maps, I ran across these US Forest Service vector tile basemaps.

Journal | Read More →
US Forest Service OSM Mashup

Using GitHub Issues as a Simple Static CMS

· 5 min read

Why use a complex CMS when you can use GitHub Issues as a lightweight blogging engine!

Journal | Read More →

Update a Git Tag

git tag -f <tag>
git push origin <tag> --force
Code | Read More →

Get Audio Info using ffmpeg

ffprobe \
  -v error \
  -select_streams a:0 \
  -show_entries stream \
  -of default=noprint_wrappers=1 \
  "input.m4a" | sed 's/=/:/'
Code | Read More →

Batch Convert Audio with ffmpeg

for f in *.flac; do ffmpeg -v warning -i "$f" -map_metadata 0 -b:a 192k "${f%.flac}.mp3"; done
Code | Read More →

Disposing an OpenLayers Map

  const layers = map.getLayers().getArray();
  layers.forEach((layer) => {
    const source = layer.getSource();
    if (source && source.dispose) {
      source.clear();
      source.dispose();
    }
    if (layer.disposeInternal) {
      layer.disposeInternal();
    } else if (layer.dispose) {
      layer.dispose();
    }
    map.removeLayer(layer);
  });
  const olCanvas = map.getViewport().querySelector("canvas");
  if (olCanvas) {
    const gl = olCanvas.getContext("webgl") || olCanvas.getContext("webgl2");
    if (gl) {
      const loseContext = gl.getExtension("WEBGL_lose_context");
      if (loseContext) {
        loseContext.loseContext();
      } 
    }
  }
  map.getInteractions().clear();
  map.getOverlays().clear();
  map.dispose();
  map.setTarget(null);
Code | Read More →

Tech Blog [Archived]

View all →

Filtering Arrays in MapLibre

· 6 min read

Techniques for Substring Matching in MapLibre GL JS and Mapbox GL JS

Tech Blog | Read More →

ShpShifter

· 2 min read

Preview, Convert, and Filter SHP, GPKG, GeoJSON & GeoParquet in the Browser

Tech Blog | Read More →

Mapping Disasters

· 2 min read

Find Open Satellite Imagery from Maxar, Capella, Umbra, and Wyvern

Tech Blog | Read More →

Palisades Fire Before and After

· 2 min read

A Sample of Available Satellite Data for Post-Disaster Visual Assessment

Tech Blog | Read More →

Deck GL and 3D Tiles

· 5 min read

Generate Colorized 3D Tiles from LiDAR and Ortho Imagery

Tech Blog | Read More →

Finding Perpendicular lines with TurfJS

· 4 min read

Bearing, LineIntersect & Destination

Tech Blog | Read More →

Photos

View all →

Folly Beach 2025

Gallery | Folly Beach 2025