
Build a Survey Grid Map with Mapbox and Township Canada
Add DLS township, section, and LSD grid layers to a Mapbox GL JS map. Search legal land descriptions, display boundaries, and identify grid cells on click.
An energy company's GIS team gets a request: build an internal web tool that shows well sites on a survey grid map. Field staff need to type in a legal land description — something like NW-25-24-1-W5 — and see exactly where that quarter section sits, with township and section grid lines visible underneath.
This is a common requirement across oil and gas, agriculture, utilities, and land management in western Canada. The DLS (Dominion Land Survey) grid is how land is recorded, and any serious map tool needs to show it.
This tutorial walks through the two core pieces: adding vector tile grid layers to a Mapbox GL JS map, and calling the Township Canada Search API to find and highlight a location by legal land description.

What you'll need
- A Mapbox access token — sign up at mapbox.com and copy your default public token
- A Township Canada API key — subscribe to the Maps API and Search API from the API page, then create a key from your account settings
Adding the survey grid
Township Canada serves the DLS grid as vector tiles. Mapbox GL JS loads vector tiles natively, so no plugins are required — you add a source and a layer, just like any other tileset.
The tile URL pattern for the DLS township grid is:
https://maps.townshipcanada.com/grid/dls/twp/{z}/{x}/{y}.mvt?api_key=YOUR_API_KEY
Grid levels each have their own tileset: grid/dls/twp for townships (~23,000 acres), grid/dls/sec for sections (~640 acres), and grid/dls/lsd for LSDs (~40 acres). Each tileset has a matching label tileset (append _label) with descriptor properties for rendering text on the grid cells.
Here's how to add the township and section grids inside a map.on('load') callback, with zoom-dependent visibility so each level appears at an appropriate scale:
map.on("load", () => {
// Township grid — visible from zoom 6 to 12
map.addSource("twp", {
type: "vector",
tiles: [`https://maps.townshipcanada.com/grid/dls/twp/{z}/{x}/{y}.mvt?api_key=${TC_API_KEY}`],
minzoom: 0,
maxzoom: 14
});
map.addLayer({
id: "ab_twp",
type: "line",
source: "twp",
"source-layer": "ab_twp",
minzoom: 6,
maxzoom: 12,
paint: { "line-color": "#2d5a47", "line-width": 1.5 }
});
// Township labels use the descriptor property
map.addSource("twp_label", {
type: "vector",
tiles: [
`https://maps.townshipcanada.com/grid/dls/twp_label/{z}/{x}/{y}.mvt?api_key=${TC_API_KEY}`
],
minzoom: 0,
maxzoom: 14
});
map.addLayer({
id: "ab_twp_label",
type: "symbol",
source: "twp_label",
"source-layer": "ab_twp_label",
minzoom: 10,
maxzoom: 12,
layout: { "text-field": "{descriptor}", "text-size": 14 },
paint: { "text-color": "#333", "text-halo-color": "#fff", "text-halo-width": 2 }
});
// Section grid — visible from zoom 12 to 14
map.addSource("sec", {
type: "vector",
tiles: [`https://maps.townshipcanada.com/grid/dls/sec/{z}/{x}/{y}.mvt?api_key=${TC_API_KEY}`],
minzoom: 9,
maxzoom: 14
});
map.addLayer({
id: "ab_sec",
type: "line",
source: "sec",
"source-layer": "ab_sec",
minzoom: 12,
maxzoom: 14,
paint: { "line-color": "#4a7c59", "line-width": 1 }
});
});
Note that vector tile features expose a descriptor property for labels (e.g., "TWP 24 RNG 1 W5"). This is the correct property to reference in text-field expressions for label layers. The Search API response uses legal_location on features returned from coordinate lookup — a different field for a different purpose.
To cover Saskatchewan and Manitoba, add layers using their source-layer names (sk_twp, mb_sec, etc.) from the same tile sources. See the Maps API vector tiles guide for the full list.
Searching by legal land description
Once the grid is on the map, users need a way to find specific locations. The Search API takes a legal land description string and returns a GeoJSON FeatureCollection with two features: the parcel boundary as a MultiPolygon, and a centroid Point identified by shape: "centroid".
async function searchAndFlyTo(query) {
const response = await fetch(
`https://developer.townshipcanada.com/v1/search/legal-location?location=${encodeURIComponent(query)}`,
{ headers: { Authorization: "Bearer " + TC_API_KEY } }
);
const data = await response.json();
if (!data.features || data.features.length === 0) return;
// Identify the centroid and polygon features
const centroid = data.features.find((f) => f.properties.shape === "centroid");
const polygon = data.features.find((f) => f.geometry.type === "MultiPolygon");
if (!centroid) return;
const [lng, lat] = centroid.geometry.coordinates;
// Fly to the location and place a marker
map.flyTo({ center: [lng, lat], zoom: 14, duration: 2000 });
new mapboxgl.Marker({ color: "#2d5a47" })
.setLngLat([lng, lat])
.setPopup(
new mapboxgl.Popup().setHTML(
`<strong>${centroid.properties.legal_location}</strong><br>` +
`${lat.toFixed(6)}, ${lng.toFixed(6)}`
)
)
.addTo(map);
// Draw the parcel boundary
if (polygon) {
if (map.getSource("search-result")) {
map.removeLayer("search-result-fill");
map.removeLayer("search-result-outline");
map.removeSource("search-result");
}
map.addSource("search-result", { type: "geojson", data: polygon });
map.addLayer({
id: "search-result-fill",
type: "fill",
source: "search-result",
paint: { "fill-color": "#2d5a47", "fill-opacity": 0.15 }
});
map.addLayer({
id: "search-result-outline",
type: "line",
source: "search-result",
paint: { "line-color": "#2d5a47", "line-width": 2 }
});
}
}
// Find a quarter section in Alberta
searchAndFlyTo("NW-25-24-1-W5");
The API base URL is https://developer.townshipcanada.com and all requests require an Authorization: Bearer YOUR_API_KEY header. The search endpoint is /v1/search/legal-location with a location query parameter.

Making grid cells clickable
For the well site tracking use case, field staff also need to click any grid cell and see its legal land description. Add a click handler on the township layer to show a popup with the cell's descriptor from the vector tile feature properties:
map.on("click", "ab_twp", (e) => {
if (e.features.length === 0) return;
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(`<strong>${e.features[0].properties.descriptor || "Township"}</strong>`)
.addTo(map);
});
map.on("mouseenter", "ab_twp", () => (map.getCanvas().style.cursor = "pointer"));
map.on("mouseleave", "ab_twp", () => (map.getCanvas().style.cursor = ""));
Attach the same handler to ab_sec and ab_lsd layers so the popup works at every zoom level.
Beyond the basics
The setup above covers the core pattern. From here you can extend the map in several directions:
Data layers — Township Canada also serves petroleum field boundaries, municipal boundaries, parks, water bodies, and road networks as vector tiles. Adding them follows the same source-and-layer pattern. A layer toggle panel lets users show and hide context layers without reloading anything.
Autocomplete search — The autocomplete endpoint at https://developer.townshipcanada.com/autocomplete/legal-location returns matching legal land descriptions as the user types. Pass a proximity parameter with the map's current centre to bias results toward the visible area — useful when field staff are already zoomed into a specific region.
Batch plotting — If you need to drop hundreds of well sites on the map at once from a spreadsheet, the Batch API can convert a list of legal land descriptions to coordinates in a single request.
NTS grids for BC — For work in British Columbia, the NTS (National Topographic System) tilesets cover the same use cases. The Maps API vector tiles guide has the full tileset list for both systems.
Next step
The Mapbox integration guide has the complete working HTML file — search box, autocomplete, layer toggles, and all grid levels — with full code you can copy, drop in your API keys, and run directly in a browser. It's a good starting point before adapting the pattern to a framework like React or Vue.
Get your API key from the API page and you can have a working survey grid map in under an hour. Questions about the API or specific use cases? The About page has contact details, and our team is familiar with the oil and gas, agri, and land management workflows this kind of tool supports.
Related guides
- Mapbox Integration Guide — Full working example with all grid levels, autocomplete, and layer toggles
- Maps API Vector Tiles — Complete tileset reference for DLS and NTS grids
- API Integration Guide — API endpoints, authentication, and key management
- API page — Subscribe to APIs and manage your keys
- What is a Legal Land Description? — Background on DLS, NTS, and how the survey systems work