Hosting Glyph Ranges for Custom Fonts
MapLibre draws labels from signed-distance-field glyphs served as protobuf files, 256 codepoints per range. Generating them is one command; the two things that go wrong are the directory name, which must equal the style’s text-font string exactly, and the font licence, which may not permit redistribution in this form at all.
When to Use This
Whenever a style’s text-font names anything other than a font your glyph host already provides. That is every custom brand font, and it is also every self-hosted deployment, since relying on a public glyph endpoint puts a third party on the critical path of every label you draw.
Specification Detail
| Element | Value |
|---|---|
| URL template | {glyphs}/{fontstack}/{range}.pbf |
{fontstack} |
The text-font array joined with commas, URL-encoded |
{range} |
0-255, 256-511, … — 256 codepoints each |
| Content type | application/x-protobuf |
| Typical range size | 40–120 KB for Latin |
A style declares the template, and MapLibre substitutes:
{ "glyphs": "https://tiles.example.com/fonts/{fontstack}/{range}.pbf" }
{ "layout": { "text-font": ["Inter Semi Bold", "Noto Sans Regular"] } }
That layer fetches /fonts/Inter%20Semi%20Bold,Noto%20Sans%20Regular/0-255.pbf. The comma-joined stack is a fallback chain: MapLibre uses the first font that supplies each glyph. Critically, the whole joined string is one path segment, so the directory on disk must be named for the joined stack, or each font’s directory must exist and the server must compose them.
Production Command
# 1. Generate ranges for each font
font-maker fonts/Inter-SemiBold.ttf "dist/fonts/Inter Semi Bold"
font-maker fonts/NotoSans-Regular.ttf "dist/fonts/Noto Sans Regular"
ls "dist/fonts/Inter Semi Bold" | head -3
# 0-255.pbf
# 256-511.pbf
# 512-767.pbf
# 2. For a static host, materialise every fontstack the style uses
jq -r '[.layers[].layout["text-font"] // empty | join(",")] | unique | .[]' style/base.json \
| while read -r STACK; do
[ -d "dist/fonts/$STACK" ] && continue # single-font stack already exists
mkdir -p "dist/fonts/$STACK"
PRIMARY=${STACK%%,*}
cp "dist/fonts/$PRIMARY"/*.pbf "dist/fonts/$STACK/" # primary font supplies the glyphs
done
# 3. Publish
aws s3 sync dist/fonts/ s3://tiles/fonts/ \
--content-type "application/x-protobuf" \
--cache-control "public, max-age=31536000, immutable"
# 4. Verify the exact URL MapLibre will request
STACK="Inter Semi Bold,Noto Sans Regular"
curl -sfI "https://tiles.example.com/fonts/$(printf %s "$STACK" | jq -sRr @uri)/0-255.pbf" \
| head -1
# HTTP/2 200
Step two is the part that catches people out on static hosting. A multi-font stack is a distinct path, and a bucket containing Inter Semi Bold/ and Noto Sans Regular/ does not answer a request for the joined stack. Copying the primary font’s ranges into a stack-named directory is the crude fix; a small glyph server that merges ranges per request is the proper one, and is worth it only when there are many stacks.
The Licensing Question
Generating glyph ranges converts a font into signed-distance-field outlines and publishes them at a URL anyone can fetch. That is redistribution, and many commercial font licences prohibit it or restrict it to specified domains.
Three practical positions. Open-licensed fonts — the SIL Open Font License covers Noto, Inter, Source Sans and most of what a map needs — permit this explicitly, and the OFL requires that the licence accompany the redistributed files. Commercial webfont licences usually cover @font-face delivery and say nothing about SDF conversion, which means asking the foundry rather than assuming. And a fully proprietary licence generally forbids it, in which case a permissively licensed lookalike is the pragmatic answer.
Whatever the outcome, record it. A glyph directory in a public bucket is a redistribution that outlives whoever set it up, and “we checked the licence” is worth a line in the repository.
Interaction Effects
With CJK and large scripts. A Latin map fetches one or two ranges. A map that must display Chinese, Japanese or Korean labels may need dozens, each a separate request. MapLibre’s localIdeographFontFamily option renders CJK from a local system font instead, which removes the requests entirely at the cost of typographic consistency — for most maps that is the right trade.
With versioning. Glyphs for a given font never change, so they can be shared across style versions and cached immutably at a stable path. This is the one asset in the whole pipeline that does not need a version prefix.
With CORS. Glyph ranges are cross-origin fetches exactly like tiles, and a bucket configured for tiles frequently misses the fonts prefix. The symptom is labels missing only when the map is served from a different origin than during development.
With the sprite. Both bundles are fetched before the first labelled frame, so both belong on the same origin as the tiles to avoid extra connection setup — see the sprite generation guide.
Performance Impact
| Script coverage | Ranges fetched | Bytes |
|---|---|---|
| Latin only | 1 | ~70 KB |
| Latin + Cyrillic + Greek | 3 | ~190 KB |
| Latin + full CJK | 40+ | 3–6 MB |
CJK via localIdeographFontFamily |
1 | ~70 KB |
The last two rows are the whole argument for local ideograph rendering. Fetching several megabytes of glyph data before labels appear is a visible delay on any connection, and the local-font fallback removes it at the cost of the glyphs not matching the brand font.
Common Mistakes
A directory name that does not match text-font exactly. Capitalisation, spacing and hyphenation all matter, and the resulting 404 removes every label with no error.
Serving PBFs as text/plain. Some proxies will attempt to transform a text response, corrupting the protobuf.
Forgetting the multi-font stack path. The individual fonts resolve, the joined stack 404s, and only the layers using that stack lose their labels.
Publishing glyphs under a version prefix. Harmless but wasteful — the same bytes get re-fetched on every release for a resource that never changes.
FAQ
Can I generate ranges from a variable font?
Instantiate the specific weight first — glyph generation wants a static font. A variable font passed directly usually produces the default instance, which may not be the weight the style expects.
How many ranges does a font produce?
As many as its codepoint coverage requires — a Latin-only font produces a handful, a full Noto build produces hundreds. Publishing all of them costs a few megabytes of static files and removes a class of failure.
Do glyph ranges need to be regenerated when the style changes?
No. They depend only on the font. A style change that switches text-font needs the new font’s ranges to exist, but nothing about existing ranges changes.
Can two styles share one glyph host?
Yes, and they should. Glyphs are font-scoped, not style-scoped, so one /fonts/ prefix serves every style and every version.
Related
- Sprite and Glyph Pipelines — the parent topic and the sprite half of the same deployment.
- Debugging Missing Icons and Fonts in MapLibre — reading the network panel when labels vanish.
- Style Validation Workflows — the dependency check that fetches these URLs in CI.
- CORS Configuration for Cross-Origin Tile Requests — the policy that must cover the fonts prefix too.