{"id":6565,"date":"2026-07-05T09:36:13","date_gmt":"2026-07-05T02:36:13","guid":{"rendered":"https:\/\/daiilynews.cu.ma\/?p=6565"},"modified":"2026-07-05T09:36:13","modified_gmt":"2026-07-05T02:36:13","slug":"mapping-with-in-memory-layers-to-reduce-llm-overload-ridgetext-blog","status":"publish","type":"post","link":"https:\/\/daiilynews.cu.ma\/?p=6565","title":{"rendered":"Mapping with In-Memory Layers to Reduce LLM Overload | RidgeText Blog"},"content":{"rendered":"<p> <br \/>\n<br \/>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 \u2014 all rendered in a single image and sent via SMS.<\/p>\n<p>Fire perimeter (shaded polygon) with a trail route overlaid. Each of these is a separate layer queued independently before the map is rendered.<br \/>\nWhile developing this layering feature, we realized one thing: you cannot pass GeoJSON through an LLM tool call, it&#8217;s simply too much data to be useful.<br \/>\nBackground<br \/>\nRidgeText is built as an orchestration layer on top of an LLM. Users interact through SMS \u2014 no app, no UI \u2014 and the LLM handles natural language understanding, decides which tools to call, and composes a clear response to send back. It&#8217;s not a rules engine; the LLM is making judgment calls at every step.<br \/>\nThat 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&#8217;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.<br \/>\nMap generation is a clear example of where this matters.<br \/>\nThe Naive Approach (and Why It Fails)<br \/>\nThe 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:<br \/>\nLLM calls get_wildfire_data() \u2192 receives 2,000 fire polygons as GeoJSON<br \/>\nLLM calls render_map(geojson: &#8230;) \u2192 passes that GeoJSON along<\/p>\n<p>In practice, a modest wildfire dataset is 50\u2013500KB of raw GeoJSON. A token is roughly 4 bytes, so 500KB is ~125,000 tokens \u2014 larger than many context windows, and expensive even when it fits.<br \/>\nThe LLM becomes a pipe for data it cannot reason about. It can&#8217;t simplify the GeoJSON, it can&#8217;t validate it, and it pays the full context cost every time.<br \/>\nThe Layer-First Pattern<\/p>\n<p>Our solution mirrors how Mapbox itself works: layers are added independently and composited at render time.<br \/>\nInstead of returning data to the LLM, each data-fetching tool stores its result server-side and returns only a lightweight acknowledgment:<br \/>\nLLM calls retrieve_wildfire_layer(location: &#8220;Cascades&#8221;)<br \/>\n\u2192 { status: &#8220;queued&#8221;, layerId: &#8220;wildfires-0&#8221;, featureCount: 847 }<\/p>\n<p>LLM calls retrieve_trail_layer(trailName: &#8220;PCT Section J&#8221;)<br \/>\n\u2192 { status: &#8220;queued&#8221;, layerId: &#8220;trail-1&#8221;, featureCount: 1 }<\/p>\n<p>LLM calls generate_map()<br \/>\n\u2192 { mapUrl: &#8220;https:\/\/storage&#8230;\/map-abc123.jpg&#8221; }<\/p>\n<p>The LLM&#8217;s context only ever sees the acknowledgments \u2014 tiny JSON objects. The GeoJSON lives in server memory until generate_map drains the queue and composites the image.<br \/>\nThe Layer Stack<br \/>\nEach retrieve_* call appends to an ordered layer array held in the request context:<br \/>\ninterface MapLayer {<br \/>\n  type: &#8216;wildfire-perimeters&#8217; | &#8216;fire-hotspots&#8217; | &#8216;trail&#8217; | &#8216;heatmap&#8217; | &#8230;;<br \/>\n  data: GeoJSON.FeatureCollection;<br \/>\n  style: LayerStyle;<br \/>\n}<\/p>\n<p>\/\/ In-process queue \u2014 lives for the lifetime of one LLM turn<br \/>\nconst layerQueue: MapLayer() = ();<br \/>\ngenerate_map renders them in insertion order \u2014 exactly like Mapbox&#8217;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.<br \/>\nOur 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&#8217;t the point \u2014 Redis, a database, or a request-scoped context object would all work. What matters is that the data lives somewhere other than the LLM&#8217;s context window.<br \/>\nWhy This Mirrors Mapbox<br \/>\nMapbox&#8217;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.<br \/>\nIt&#8217;s worth being precise about what &#8220;Mapbox&#8221; means in our renderer. We fetch a Mapbox Static API image as the base tile \u2014 terrain, roads, labels \u2014 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&#8217;s format not because the renderer requires it, but as a deliberate forward-looking choice.<br \/>\nIf we ever outgrow static tiles \u2014 for 3D terrain, complex blending, or animated layers \u2014 we can swap the renderer for a headless Mapbox GL JS instance running in a Playwright browser. That renderer would consume the exact same layer queue without any changes to the tools or the LLM&#8217;s interface. The cost and speed profile would also change: a JavaScript-based renderer can cache tiles and GL assets across requests, potentially reducing per-map costs and improving cold-start latency compared to making a fresh Static API call for every map.<br \/>\nThis means:<\/p>\n<p>Adding a new data source is adding a new retrieve_* tool \u2014 the render pipeline doesn&#8217;t change<br \/>\nLayer ordering is natural to express through tool call sequence<br \/>\nStyling is co-located with the layer type, not passed through the LLM<br \/>\nThe renderer is swappable \u2014 static tiles today, headless GL tomorrow \u2014 without touching the LLM layer<\/p>\n<p>The Render Step<br \/>\ngenerate_map is deterministic. It receives no GeoJSON from the LLM \u2014 only optional parameters like zoom level or map style. It reads the layer queue, projects each feature set onto a Mapbox Static API base image, and composites them using sharp.<br \/>\nasync function generateMap(options: MapOptions): Promisestring> {<br \/>\n  const layers = drainLayerQueue();           \/\/ consume and clear<br \/>\n  const base = await fetchMapboxBase(options);<br \/>\n  const composed = await compositeLayers(base, layers);<br \/>\n  return await uploadToStorage(composed);<br \/>\n}<br \/>\nThe result is a single image URL the LLM can attach to its SMS response.<br \/>\nWhat the LLM Actually Sees<br \/>\nFor a &#8220;show me wildfires near the PCT&#8221; request, the LLM&#8217;s tool call sequence looks like:<br \/>\n(<br \/>\n  { &#8220;name&#8221;: &#8220;retrieve_wildfire_layer&#8221;, &#8220;result&#8221;: { &#8220;status&#8221;: &#8220;queued&#8221;, &#8220;featureCount&#8221;: 847 } },<br \/>\n  { &#8220;name&#8221;: &#8220;retrieve_trail_layer&#8221;,    &#8220;result&#8221;: { &#8220;status&#8221;: &#8220;queued&#8221;, &#8220;featureCount&#8221;: 1   } },<br \/>\n  { &#8220;name&#8221;: &#8220;generate_map&#8221;,           &#8220;result&#8221;: { &#8220;mapUrl&#8221;: &#8220;https:\/\/&#8230;&#8221;               } }<br \/>\n)<br \/>\nTotal tokens in tool results: ~150. Without this pattern: ~125,000+.<br \/>\nTradeoffs<br \/>\nWhat you gain:<\/p>\n<p>Context window stays small regardless of dataset size<br \/>\nRender pipeline is deterministic and testable in isolation<br \/>\nAdding new layer types doesn&#8217;t require changing how the LLM interacts with the system<\/p>\n<p>What you give up:<\/p>\n<p>The LLM can&#8217;t reason about the underlying geometry of any layer it queued. It knows a trail was added and how many features it contains, but nothing about the coordinates themselves. A question like &#8220;what&#8217;s the nearest city to my trail?&#8221; can&#8217;t be answered precisely from the queued layer data \u2014 the LLM would need a separate tool that returns that as text. It can, however, view the rendered map image later via the Storage link and make visual observations about the composite result.<br \/>\nLayer queue is ephemeral \u2014 it doesn&#8217;t survive across turns, so multi-turn map refinement requires re-fetching<\/p>\n<p>The second tradeoff could be addressed by persisting layers to a database table that follows the same history retention policies as conversation responses. Each rendered map response would store its associated layers by reference, so if the user follows up \u2014 asking to zoom in, change the style, or add another data source \u2014 the layers can be rehydrated directly from the database without re-fetching from the original APIs.<br \/>\nApplying This to Your System<br \/>\nThe pattern here isn&#8217;t about maps. It&#8217;s about recognizing when your LLM is acting as a data pipe instead of a reasoning engine \u2014 and removing it from that role.<br \/>\nThe signal to look for: Tool A fetches data \u2192 LLM receives it \u2192 LLM passes it directly to Tool B. If the LLM isn&#8217;t making a decision based on the content, it shouldn&#8217;t be holding the content at all.<br \/>\nA few scenarios where the same approach applies:<br \/>\nMulti-source data enrichment. NREL provides a dataset of EV charging stations, but it&#8217;s missing amenity data, real-time availability, and user reviews that other APIs carry. Rather than fetching each source and passing the merged payload back to the LLM for combination, each retrieval tool queues its dataset server-side. A compositor joins them on station ID and produces the enriched result. The LLM orchestrates which sources to pull and receives a summary \u2014 it never holds the intermediate per-source payloads or the merged dataset.<br \/>\nLog analysis with multiple passes. A user asks &#8220;how many errors did we have in the last hour, and what are the primary issues?&#8221; That&#8217;s two analytical questions over the same underlying data. Without this pattern, you&#8217;d fetch the logs twice \u2014 once per tool call \u2014 or pass the raw log payload back to the LLM between calls. Instead, the log fetch runs once and stores the result. An error-count tool and a root-cause tool both read from the same stored dataset independently. The LLM gets two small structured answers rather than raw log data that it has no business holding.<br \/>\nAny ETL pipeline where the output is what matters. If you&#8217;re pulling from multiple upstream sources and compositing them into a single result \u2014 a report, a dashboard dataset, a ranked list \u2014 the LLM&#8217;s role is to decide what to pull and describe the result, not to participate in the merge. The intermediate state between sources belongs in your pipeline, not in the context window.<\/p>\n<p><br \/>\n<br \/><a href=\"https:\/\/ridgetext.com\/blog\/mapbox-llm-composition\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 \u2014 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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":6566,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[676],"tags":[2351,2349,2356,2348,2347,2355,2354,2350,2346,2352,2353],"class_list":["post-6565","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tech-ai","tag-adventure-companion","tag-hiking-assistant","tag-offline-ai","tag-outdoor-ai","tag-ridgetext","tag-satellite-ai","tag-satellite-texting-ai","tag-sms-ai","tag-sms-ai-assistant","tag-trail-guide","tag-wilderness-safety"],"_links":{"self":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts\/6565","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6565"}],"version-history":[{"count":0,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts\/6565\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/media\/6566"}],"wp:attachment":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6565"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6565"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6565"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}