Getting fonts onto the map

osm
lmb
labeling
Author

Toby Champion

Published

November 27, 2025

Building a Community Mapping Platform: Part 2

An unusual font on the map

For the prototype map for my Let’s Map Bainbridge project, I wanted to have at least one unusual font on the map. Perhaps a font that looks like the sort of fonts traditionally used for features on the coastline, that would look good on the screen. I found Eben Sorkin’s Merriweather font family, which has an Open Font License, so I can use it for free. I chose just Merriweather Light Italic to get us going.

Making the font available to MapLibre

This is at the top level of lmb-prototype.json:

"glyphs": "/font/{fontstack}/{range}.pbf",

The glyphs value is a template for the URL MapLibre will use to fetch font data, where {fontstack} is (at its simplest) the name of the font (Merriweather Light Italic), and {range} is, for example, 512-767.pbf.

So the URL might be: /font/Merriweather Light Italic/512-767.pbf.

What those *.pbf files are

These files aren’t the TTF font file I downloaded from Google Fonts, but data representing what each character (or “glyph”) looks like. The data is in an extremely compact, binary format: a Protobuf Binary Format (PBF) file.

To generate the glyph files from the TTF file, I used the MapLibre project’s Font Maker service. There’s a command-line tool available, too, if you really must.

The number is a code point, which is a number refering to a particular entry in a table. In this case, what’s in the table is glyphs for each letter in the alphabet (and so much more): ‘B’, ‘l’, ‘a’ and so on. A code point, written in hexadecimal, can be prefixed with U+ to make it clear that it’s a code point.

A few code points for you, generated by this bit of Python code:

import unicodedata

def show_unicode_chars(text):
    for char in text:
        code_point = f"U+{ord(char):04X}"
        name = unicodedata.name(char)
        print(f"- {code_point} ({name}): {char}")

show_unicode_chars("Blakely: 67 🎉")
- U+0042 (LATIN CAPITAL LETTER B): B
- U+006C (LATIN SMALL LETTER L): l
- U+0061 (LATIN SMALL LETTER A): a
- U+006B (LATIN SMALL LETTER K): k
- U+0065 (LATIN SMALL LETTER E): e
- U+006C (LATIN SMALL LETTER L): l
- U+0079 (LATIN SMALL LETTER Y): y
- U+003A (COLON): :
- U+0020 (SPACE):  
- U+0036 (DIGIT SIX): 6
- U+0037 (DIGIT SEVEN): 7
- U+0020 (SPACE):  
- U+1F389 (PARTY POPPER): 🎉
Source: unicode.ipynb

Each file on the server, such as 512-767.pbf, contains the symbols for 256 code points. The file 0-255.pbf file has all we need for basic Latin characters. But 🎉, which is U+1F389, and 127881 in decimal. So that would need a different file. Which one? Let’s do the math:

def filename_for_codepoint(point):
    start = point // 256 * 256
    end = start + 255
    return f"{start}-{end}.pbf"
    
filename_for_codepoint(127881)
'127744-127999.pbf'

Using the font in a layer

So now we can use the font in a layer:

"text-font": [
    "Merriweather Light Italic"
],

Creating a visual hierarchy for the labels

Cartographers want a nice visual hierarchy for labels, but using different font families altogether, or just varying the size or color of a particular font, or using bold or italics. With just this one variant of the font (Light Italic), I’m relying on font size to do all the work.

There are four levels in the hierarchy for labels around the coastline:

  1. Blakely Harbor, Eagle Harbor
  2. Bill Point, Hawley Cove
  3. Hornbeak Spit, Midden Point
  4. The Mill Pond at Blakely Harbor

The font size is fixed for each of these levels (19pt, 15pt, 13pt, 10pt). Eventually the font size for each level will need to change depending on the zoom level (and each level should only be visible at a particular range of zoom levels).

In the style file this looks like:

"text-size": [
    "match",
    ["get", "level"],
    1,
    19,
    2,
    15,
    3,
    13,
    4,
    10,
    30
    ],

That 30pt default is there just in case I forget to allocate a level to each of these labels: it’ll make the mistake really obvious, so I can fail fast. These labels are my own dataset, not coming from OpenStreetMap.