Mapping with In-Memory Layers to Reduce LLM Overload

Buckwheat4691 pts0 comments

Mapping with In-Memory Layers to Reduce LLM Overload | RidgeText Blog | RidgeText SMS AI<br>mapboxllmarchitecturemapping<br>Mapping with In-Memory Layers to Reduce LLM Overload<br>How we built a Mapbox-compatible map compositor where the LLM orchestrates layers without ever touching GeoJSON — keeping context windows small and rendering deterministic.<br>Allan Bogh·June 18, 2026

If you ask RidgeText to generate a fire perimeter map with trails overlaid, the response now includes a map with fire perimeters and your trail route layered on top — all rendered in a single image and sent via SMS.

Fire perimeter (shaded polygon) with a trail route overlaid. Each of these is a separate layer queued independently before the map is rendered.

While developing this layering feature, we realized one thing: you cannot pass GeoJSON through an LLM tool call , it's simply too much data to be useful.

Background

RidgeText is built as an orchestration layer on top of an LLM. Users interact through SMS — no app, no UI — and the LLM handles natural language understanding, decides which tools to call, and composes a clear response to send back. It's not a rules engine; the LLM is making judgment calls at every step.

That non-determinism is a feature for conversation, but it creates a constraint for features: anything the LLM touches needs to be resilient to variation. If a tool returns too much data, the LLM may truncate, hallucinate a summary, or fail silently. If a tool's interface is ambiguous, the LLM may call it incorrectly. Good tool design means shaping what the LLM sees so that the range of reasonable responses all lead to correct outcomes.

Map generation is a clear example of where this matters.

The Naive Approach (and Why It Fails)

The obvious implementation is to have a tool that fetches fire data and returns it to the LLM, then a second tool that accepts that data and renders a map. Something like:

LLM calls get_wildfire_data() → receives 2,000 fire polygons as GeoJSON<br>LLM calls render_map(geojson: ...) → passes that GeoJSON along

In practice, a modest wildfire dataset is 50–500KB of raw GeoJSON. A token is roughly 4 bytes, so 500KB is ~125,000 tokens — larger than many context windows, and expensive even when it fits.

The LLM becomes a pipe for data it cannot reason about. It can't simplify the GeoJSON, it can't validate it, and it pays the full context cost every time.

The Layer-First Pattern

Our solution mirrors how Mapbox itself works: layers are added independently and composited at render time.

Instead of returning data to the LLM, each data-fetching tool stores its result server-side and returns only a lightweight acknowledgment:

LLM calls retrieve_wildfire_layer(location: "Cascades")<br>→ { status: "queued", layerId: "wildfires-0", featureCount: 847 }

LLM calls retrieve_trail_layer(trailName: "PCT Section J")<br>→ { status: "queued", layerId: "trail-1", featureCount: 1 }

LLM calls generate_map()<br>→ { mapUrl: "https://storage.../map-abc123.jpg" }

The LLM's context only ever sees the acknowledgments — tiny JSON objects. The GeoJSON lives in server memory until generate_map drains the queue and composites the image.

The Layer Stack

Each retrieve_* call appends to an ordered layer array held in the request context:

interface MapLayer {<br>type: 'wildfire-perimeters' | 'fire-hotspots' | 'trail' | 'heatmap' | ...;<br>data: GeoJSON.FeatureCollection;<br>style: LayerStyle;

// In-process queue — lives for the lifetime of one LLM turn<br>const layerQueue: MapLayer[] = [];<br>generate_map renders them in insertion order — exactly like Mapbox's layer stack, where earlier layers sit below later ones. The LLM controls ordering implicitly by the sequence it calls the tools: if it calls retrieve_satellite_base before retrieve_trail, the trail draws on top of the satellite imagery.

Our implementation uses an in-process Map keyed by session ID with a 30-minute TTL, so layers that were queued but never rendered are evicted automatically rather than accumulating. The specific mechanism isn't the point — Redis, a database, or a request-scoped context object would all work. What matters is that the data lives somewhere other than the LLM's context window.

Why This Mirrors Mapbox

Mapbox's core model is: sources provide data, layers define how to render it, and layers compose in declaration order. Our server-side queue is the same abstraction, just running without a browser.

It's worth being precise about what "Mapbox" means in our renderer. We fetch a Mapbox Static API image as the base tile — terrain, roads, labels — and then composite the data layers on top of it using canvas. Mapbox itself never sees the GeoJSON; it only provides the background. The layer descriptors we store follow Mapbox's format not because the renderer requires it, but as a deliberate forward-looking choice.

If we ever outgrow static tiles — for 3D terrain, complex blending, or animated layers — we can swap the renderer for a headless Mapbox GL JS instance running...

layers data mapbox geojson calls context

Related Articles