A map view has layers, and they have wildly different densities: four hundred buildings, thirty road segments, two zoning polygons. A single feature limit hands the whole budget to the densest layer, which is almost never the one holding the answer. This guide allocates the budget deliberately, as the layer-aware half of context-window optimization for maps.
When to Use This Approach
Allocate per layer whenever more than one layer is in scope. For a single-layer view a global limit is equivalent and simpler.
| Question | Layer weighting | Reason |
|---|---|---|
| “What is at this address” | Buildings high, zoning low | The answer is a specific feature |
| “Can I build here” | Zoning and constraints high | Buildings are context, not answer |
| “What is this area like” | Even, favouring variety | Character comes from the mix |
| “Where does the boundary run” | One layer, high detail | Everything else is a distraction |
| Unclassified | Even, with floors | Fails least badly |
The fourth row is worth separating out because it inverts the usual advice. When a question is about one layer’s geometry, spending budget on other layers actively harms the answer by consuming detail the subject needed.
Implementation
The allocator distributes the geometry budget by weight, guarantees every requested layer a floor, and returns the allocation so it can be logged and explained.
import logging
from dataclasses import dataclass
from typing import Mapping
log = logging.getLogger("layer_budget")
FLOOR_DIVISOR = 4 # every layer gets at least budget / (layers * 4)
@dataclass(frozen=True)
class Allocation:
per_layer: dict[str, int]
floor: int
note: str
def allocate(available: Mapping[str, int], weights: Mapping[str, float],
geometry_budget: int) -> Allocation:
"""Split a geometry budget across layers by weight, with a floor for each."""
layers = [name for name, count in available.items() if count > 0]
if not layers:
return Allocation({}, 0, "no layer has any feature in view")
if geometry_budget <= 0:
raise ValueError("geometry budget must be positive")
floor = max(1, geometry_budget // (len(layers) * FLOOR_DIVISOR))
total_weight = sum(max(0.0, weights.get(name, 0.0)) for name in layers)
if total_weight <= 0:
even = geometry_budget // len(layers)
log.info("no usable weights supplied — allocating evenly")
return Allocation({name: even for name in layers}, floor,
"even allocation: no weights supplied")
raw = {name: max(floor, int(geometry_budget * max(0.0, weights.get(name, 0.0)) / total_weight))
for name in layers}
# Floors can push the total over budget; trim proportionally from the layers above floor.
overshoot = sum(raw.values()) - geometry_budget
while overshoot > 0:
trimmable = [n for n in layers if raw[n] > floor]
if not trimmable:
break # every layer is at its floor: accept and log
for name in sorted(trimmable, key=lambda n: -raw[n]):
if overshoot <= 0:
break
take = min(raw[name] - floor, max(1, overshoot // len(trimmable)))
raw[name] -= take
overshoot -= take
note = "" if overshoot <= 0 else f"floors exceed the budget by {overshoot} tokens"
if note:
log.info("layer floors exceed the geometry budget by %d", overshoot)
return Allocation(raw, floor, note)
The floor is the load-bearing part. Without it a weighting that reflects the question — buildings at 0.7, zoning at 0.05 — gives the zoning layer a handful of tokens and effectively excludes it, which is precisely the failure the per-layer approach exists to prevent. With it, a low-weighted layer still contributes one or two features, and one zoning polygon is frequently the whole answer.
Accepting an overshoot when every layer sits at its floor is also deliberate. That condition means the budget is too small for the number of layers requested, and the honest response is to report it rather than to silently drop a layer.
Validation & Testing
def test_every_layer_with_features_gets_at_least_the_floor():
a = allocate({"buildings": 400, "roads": 30, "zoning": 2},
{"buildings": 0.7, "roads": 0.25, "zoning": 0.05}, 8000)
assert all(v >= a.floor for v in a.per_layer.values())
def test_empty_layers_are_excluded_from_the_split():
a = allocate({"buildings": 400, "hydrology": 0}, {"buildings": 1.0}, 4000)
assert "hydrology" not in a.per_layer
def test_total_does_not_exceed_the_budget_when_it_can_be_met():
a = allocate({"a": 10, "b": 10, "c": 10}, {"a": 0.8, "b": 0.15, "c": 0.05}, 3000)
assert sum(a.per_layer.values()) <= 3000
def test_impossible_budget_is_reported_not_hidden():
a = allocate({f"l{i}": 5 for i in range(12)}, {}, 24)
assert a.note and "exceed" in a.note
def test_missing_weights_fall_back_to_even():
a = allocate({"a": 5, "b": 5}, {}, 1000)
assert a.per_layer["a"] == a.per_layer["b"]
The fourth test is the one worth keeping through refactors. It asserts that an over-constrained allocation is visible rather than resolved by quietly dropping layers, which is what every straightforward implementation does when the arithmetic stops working.
Run these against the real weight table rather than fixtures of it. The weights are configuration that changes as question classes are added, and a test that supplies its own weights keeps passing long after the production table has acquired a class with no entry — which silently falls through to an even split.
Gotchas & Edge Cases
Weights derived from layer density. Weighting by how many features a layer has re-creates the problem the allocator exists to solve. Weights come from the question class, never from the data.
A floor set too high. With a dozen layers and a small budget, floors alone can consume everything and the weighting stops mattering. Scale the floor with the layer count, as above, and report when it binds.
Layers with features that are individually enormous. A single national boundary can exceed a whole layer’s allocation. Reduce within the layer first — the tokenizer’s reduction ladder handles this — and only then treat the layer as over budget.
Allocation computed before the question is classified. The weights depend on the question class, so an allocator called too early in the pipeline gets defaults every time. Classify first, allocate second.
Empty layers counted in the split. A layer with no features in view should not receive a floor; giving it one takes tokens from layers that could use them. Filter before allocating, as the implementation does.
A weight table with no entry for a class. The lookup falls through to an even split, which is a reasonable default and is not what anyone intended for that class. Log the fall-through and treat a recurring one as a missing table row.
Allocation not reported. The per-layer split is the single most useful line in a prompt-assembly log, because it explains at a glance why an answer talked about buildings and not zoning. Log it with the question class beside it.
Frequently Asked Questions
Where should layer weights come from?
A small table keyed on question class, hand-written and reviewed rather than learned. There are rarely more than half a dozen classes worth distinguishing, the weights are easy to reason about, and a table is inspectable in a way a fitted model is not. Learn them only if you have a labelled set large enough to fit them and the discipline to refit when the question mix changes.
Should the allocation be in tokens or in features?
Tokens, because features vary enormously in cost — a building footprint and a coastline differ by two orders of magnitude. Allocating features gives a layer of large geometries an unbounded share of the budget while appearing fair. Convert to features inside each layer, using measured token costs, once the token allocation is known.
What happens when a layer cannot use its allocation?
Return the surplus and redistribute. A layer with two small polygons will not spend a large allocation, and leaving the tokens unused wastes budget that the dense layers would happily absorb. A second pass that redistributes unspent allocation in weight order is a few lines and recovers a useful fraction on most requests.
Does this interact with retrieved prose?
Only through the split between prose and geometry made one level up. Once that split is fixed, this allocator divides the geometry half and never touches the prose. Keeping the two decisions separate is what makes both explainable: one answers "how much of the window is geometry", the other answers "which layers get it".
How should a layer that was omitted entirely be reported?
By name, in the prompt header, alongside the count it would have contributed. A model told that a hydrology layer exists in the view but was not included can say so when a question turns out to be about water; one that simply never saw the layer will answer as though the view contained no water features at all. This costs a handful of tokens and removes a whole class of confidently incomplete answers.
Is it worth allocating differently for follow-up questions?
Yes, and it is one of the clearest wins available. A follow-up narrows the subject — the user has asked about zoning specifically — so the weighting for that turn should shift decisively toward the layer they asked about. Treating every turn as an independent request with the same weights wastes most of the budget re-establishing context the conversation already has.
Related
- Up to the parent topic: Context-Window Optimization for Maps
- Managing Context Limits with Quadtree Indexes
- Related topic: Geometry Tokenization Strategies
- Related topic: Cost and Latency Budgets for Spatial Agents