Define your own hierarchical region layout — campaign zones, sales territories, underwriting books, school clusters, anything you'd organize on a whiteboard — then tag a donor file, customer list, or policy book against it. Every row comes back with its leaf region plus the full ancestor chain. Aggregate, render, publish. The whole loop is six MCP calls. Premium feature.
A region group is a named, hierarchical layout built on top of any boundary set you can query — system collections (us-states, us-counties, us-cd119, us-tracts, us-zcta, us-school-unified, us-sldu / us-sldl) or workspace-owned custom polygons you uploaded via ingest_geojson.
Inside a group: as many regions as you want, organized as a tree. Top-level "master" regions can contain "sub-regions"; sub-regions can contain their own sub-regions. Each leaf region owns a list of base-shape external IDs (county FIPS, CD number, ZCTA, or polygon IDs — whichever the group's base collection uses). Re-shape it any time without re-ingesting the underlying boundaries.
A field-organizing director runs a 50,000-row donor file. They want every donor tagged by:
The whole layout is six steps; the resulting /v/<slug> map plus tagged dataset stays around forever as the team's source of truth.
The Census Bureau's 4 regions × 9 divisions × 51 states (50 + DC) is the canonical demographic hierarchy. Clone it as a starting point; rename and reshape as needed.
mcp.call("clone_region_template", {
template: "census-regions-and-divisions",
new_slug: "campaign-2026-territory",
new_name: "Campaign 2026 territory",
})
→ region_group: "campaign-2026-territory"
13 regions created (4 masters + 9 sub-regions)
51 members added (every state + DC, FIPS '01' through '56')
Maybe your "East" team covers more than the Census definition — you want NJ + NY + PA + DC + MD + DE in the same sub-region under East. Drop a region's members, re-add the right ones, done.
mcp.call("remove_shapes_from_region", {
region_group: "campaign-2026-territory", region: "mid-atlantic",
shape_external_ids: ["34", "36", "42"], // remove NJ, NY, PA from default mid-atlantic
})
mcp.call("define_region", {
region_group: "campaign-2026-territory",
slug: "east-coast-corridor", name: "East-Coast Corridor",
parent_region: "northeast",
})
mcp.call("add_shapes_to_region", {
region_group: "campaign-2026-territory", region: "east-coast-corridor",
shape_external_ids: ["34","36","42","11","24","10"], // NJ, NY, PA, DC, MD, DE
})
Or build your layout from scratch with create_region_group + define_region + add_shapes_to_region. The dashboard at /dashboard/regions/<slug> has a click-to-add gallery so you don't have to type FIPS codes by hand on mobile.
mcp.call("render_region_group_map", {
region_group: "campaign-2026-territory",
color_by: "master_region", // East / Midwest / South / West get distinct colors
title: "Campaign 2026 territory",
})
→ SVG showing the U.S. colored by 4 master regions
Color assignment is deterministic per region slug — re-rendering the same group produces the same colors every time. Citation-grade.
Donor file, customer list, policy book, whatever. The Census batch geocoder handles 10K addresses per call; chain multiple if you have more.
mcp.call("bulk_match_addresses", {
records: donors,
collection: "us-states", version: "tiger-2024",
})
// → one row per donor, each tagged with the containing state FIPS
mcp.call("tag_records_with_region_group", {
region_group: "campaign-2026-territory",
records: geocoded_donors, // each carries shape_external_id from step 4
})
// → records: each annotated with its containing region:
// [
// {
// id: "",
// shape_external_id: "37", // e.g. NC
// region: {
// slug: "south-atlantic",
// name: "South Atlantic",
// ancestor_slugs: ["south"],
// ancestor_names: ["South"]
// }
// },
// ...
// ]
// Unmatched rows (shape_external_id outside the region group) are
// returned in a separate unmatched array.
Each row now carries both levels: the leaf (sub-region) and the full ancestor chain. Pivot in either direction.
Group the tagged records by region slug, count donors, sum donations. Ingest the aggregate as a dataset, render a choropleth.
// Client-side aggregation:
const byRegion = {};
for (const d of tagged_donors) {
const key = d.region.slug;
byRegion[key] = (byRegion[key] || 0) + d.amount;
}
mcp.call("ingest_dataset", {
slug: "donations-by-region-2026q1",
records: Object.entries(byRegion).map(([region_slug, total]) => ({
region_slug, total_donations: total,
})),
collection: "us-states", version: "tiger-2024", // match the base
})
mcp.call("render_map", {
collection: "us-states", version: "tiger-2024",
dataset: "donations-by-region-2026q1", field: "total_donations",
palette: "sequential",
title: "Q1 2026 donations by campaign region",
subtitle: "Per-region totals, palette by quantile",
})
mcp.call("publish_map", { view_id: …, freeze: true })
→ /v/campaign-2026-donations-q1
Tagging a contact file by hand-rolled territory layout used to mean: export to a spreadsheet, write a state → region VLOOKUP table, hand-update it when the regions shift, re-merge with the contact file every refresh cycle. Mid-cycle re-shaping (a single state moves teams) breaks the linkage; the operations director ends up doing weekly reconciliation. The region group lives in the database; reshape it once and every dataset that tagged against it remains queryable.
region_groups, regions (self-referencing parent_region_id), region_members.region_groups.updated_at fresh whenever any region or member changes — dashboards sort by "recently modified."define_region rejects parent assignments that would create a loop in the hierarchy.list_region_groups, list_regions, get_region_tree) stay standard.
Tools used: clone_region_template, remove_shapes_from_region, define_region, add_shapes_to_region, render_region_group_map, bulk_match_addresses, tag_records_with_region_group, ingest_dataset, render_map, publish_map. The "write" half is premium-gated; reading existing groups stays standard.