How it is built

For anyone who wants to know how it works. Every performance figure was measured on the machine that runs the site.

The pipeline

  1. Download the zip files from the open data platform, about 104 MB for three years
  2. Read the CSVs straight out of the zip with Python's zipfile, never extracting them — unpacked they are 1.5 GB
  3. De-duplicate, remove fallback coordinates
  4. Build SQLite: one grid of 12 metre cells with a month dimension, plus pre-aggregated tables for zoom levels 7 to 11
  5. The result is 385 MB

Why not store one grid per zoom level

The first version did exactly that: twelve levels stored separately, 180 MB for a single year. Switching to one fine grid rolled up with integer division at query time tripled the data covered (one year became two years and eight months) while shrinking the database to 385 MB. Only the low zoom levels, 7 to 11, are still pre-aggregated, because at that range a query would otherwise have to scan too many cells.

Tiles instead of a bounding box

The first API took a bounding box. The problem is that a bounding box changes with every decimal place, so panning slightly refetches the whole viewport and neither the browser nor a CDN can cache any of it. One response measured 954 KB.

With fixed tile URLs instead:

Tiles carry Cache-Control: max-age=86400. The data changes twice a month, so a day is safe.

Compression

GeoJSON is highly repetitive text and compresses extremely well. Measured on the same 1,044,568 byte response:

gzip levelCompressedRatioServer compress Browser decompress
169,13215.1×1.8 ms1.28 ms
458,19417.9×2.6 ms1.25 ms
653,25519.6×4.3 ms1.19 ms
950,96720.5×10.4 ms1.27 ms

Decompression sits at about 1.2 ms regardless of level, because that is native code inside the browser. Level 4 was chosen: 8% larger than level 6, but twice as fast to produce.

Front end

The map is MapLibre GL JS, the open-source fork of Mapbox GL, BSD licensed. It was chosen because it has a native heatmap layer, renders on the GPU, and needs neither an API key nor a credit card.

Google Maps' HeatmapLayer was deprecated in May 2025 and removed in version 3.65 in May 2026, with third-party libraries suggested instead. Google Maps also bills per load, which is a risk for a free tool.

Base tiles come from OpenFreeMap, again with no API key and no usage limits. Heatmap weights are computed in the browser, because tiles have to be pure data to stay cacheable, and normalising each tile separately would leave visible colour seams along the tile boundaries.

Back end

Flask and SQLite, in the same container as the other tools on this site but touching no GPU — no queue, no quota, and requests can run concurrently without limit.

Measured query time: 8 to 82 ms per tile depending on zoom level and extent.

Back to the map

Other tools on this site

All run on one machine at home; the data tools use Taiwan government open data and state their limits.