November 2026 · South Africa · By Mo Khan

Cover illustration generated with AI.
I had never built a mapping application before Intersection Intelligence.
I knew the problem I wanted to solve. I could look at a summary of infrastructure across a region, but I could not follow that summary down to a particular intersection and reliably answer a few ordinary questions. What is here? Who is responsible for it? What evidence supports that answer? When someone learns something new about the site, where does that knowledge go?
A spreadsheet could tell me a total. I wanted to be able to point at a place.
That gap became Intersection Intelligence: my personal map-learning experiment and an internal research application for exploring road intersections, understanding the evidence around them, and capturing knowledge that would otherwise remain scattered across documents and people's heads. It now brings together an interactive atlas, site dossiers, corridor views, analytical comparisons, reviewed contributions and contextual AI chat.
It is not being used commercially. I am sharing it as an account of learning a new technical domain, rather than announcing a commercial offering.
I built it with Codex and Claude. I brought the product intent, the domain questions and the decisions about what mattered. The agents helped me work through an unfamiliar geospatial stack, implement it, challenge the design and test what we had actually delivered. This was a substantial extension of my existing software and systems experience into a domain I had not built in before.
Looking back through the git history, the interesting story is how often the meaning of a seemingly simple feature changed once we tried to make it trustworthy.
The experiment started with a question about a place
The early vision was much bigger than a map with coloured pins. I wanted a useful digital representation of infrastructure: the road context around a junction, the equipment recorded there, the organisations with different responsibilities, and the evidence we still needed to gather.
The longer-term ambition includes richer network analysis and better support for planning. The delivered research application is a discovery-and-capture alpha. It does not operate traffic signals, ingest a universal live traffic feed, or demonstrate that a proposed intervention would improve traffic flow. Those capabilities would require additional data and validation. Keeping that boundary visible has become part of the application's design.
The workflow I kept coming back to was straightforward. Start with a country or area. Find a junction or corridor. Open its dossier. Understand what the sources say. Add or review evidence. Compare the resulting picture with another area, or ask a question in the context already on screen.
That last part matters. A useful map should help someone carry a question through the application without continually restating where they are looking.

The journey starts at country scale. These South African screenshots were captured from the signed-in research app on 14 November 2026 in the OSM junction view. Company and provider names, commercial summaries and installation counts are obscured. Sensitive UI is covered with deterministic pixel masks; the original map pixels, labels and geometry are unchanged. Map data © OpenStreetMap contributors; Protomaps cartography. Published administrative boundaries are reference geography, not cadastral boundaries.
Late August: a prototype became a real geography problem
The design documents had been evolving for months before the first standalone implementation landed on 30 August. That first slice used a deterministic demonstration dataset. It let us exercise the service, authentication, edits and interface without pretending we had already solved national geography processing.
The next step exposed the difference immediately. On 31 August we began proving the application against real OpenStreetMap data in Gauteng. The early browser implementation went through a succession of rendering repairs: limiting visible objects, clustering points, avoiding repeated coordinate work and making the dossier respect the selected scope.
The git log is wonderfully unromantic about this. There are commits for getting the map to initialise, making clusters usable and ensuring the first preview actually paints. These are the details that disappear in a polished demo, but they determine whether someone can use the thing.
By 1 September, the atlas had moved to MapLibre GL JS and a self-hosted PMTiles basemap. MapLibre supplies the browser renderer. PMTiles packages tiled geography into an archive whose readers can fetch the relevant byte ranges on demand. That helped us serve the map without transferring an entire country's geography to the browser. MapLibre documentation and the PMTiles format guide explain those building blocks.
I was learning the difference between the basemap and the application's own evidence. The streets and labels provide geographic context. The records layered over them have their own identities, source dates and access rules. A fast basemap cannot compensate for a slow dossier endpoint or a misleading count.

Country to province: the same interface now shows Gauteng. Zoom changes the amount and form of visible geographic evidence. Provider names and commercial values are redacted.

Province to city: Johannesburg becomes the working area. The shortlist and map must agree about the selected scope. Map data © OpenStreetMap contributors; Protomaps cartography.

City to corridor: selecting a named route makes a connected stretch of road the focus. This is a view of published route geography, not a simulation of traffic flow. Map data © OpenStreetMap contributors; Protomaps cartography.
We also learned early that building geography and serving geography were different workloads. The first real extraction experiments exceeded the small application's memory budget. The architecture changed accordingly: prepare and validate the heavy datasets away from the serving worker, then publish versioned artifacts for the application to read.
That was one of the first important decisions AI helped me reach. It came from running the work and measuring it, rather than assuming the initial hosting plan would cope.
The hardest question was what one dot meant
Consider an illustrative junction. OpenStreetMap may describe several signal nodes and the roads around them. An authority register may describe an equipment installation. A colleague may have a recent observation about the same location. A regional spreadsheet may contain an aggregate count that includes it.
Those sources describe different things. Adding their rows together would produce a confident-looking number with no coherent meaning.
The application therefore keeps several kinds of evidence distinct:
| Evidence | What it can establish | What it cannot establish by itself |
|---|---|---|
| OpenStreetMap geography | Mapped locations, road structure and recorded tags | Complete equipment ownership or current operational status |
| Authority records | What a particular source reports about an asset | A certain match to a mapped junction when the link is ambiguous |
| Human contributions | An attributable observation or reviewed correction | An automatic rewrite of the original source |
| Regional summaries | Counts at their stated area and reporting date | Coordinates for individual sites |

The public explanation of the evidence model. Examples are generic. No commercial populations or installation records are shown.
We made a particularly important rule explicit: an aggregate count must never become a collection of invented map points. If a source reports more assets than we can position, that difference remains a coverage gap.
We also separated unknown from zero. An empty field does not prove that a piece of equipment is absent. A source that has not been loaded does not establish that an area has no assets. An automated proximity match is a proposal until there is sufficient evidence to accept it.
This is where the software became much more interesting to me. The map made disagreements visible. The real product work was deciding how to preserve those disagreements without making the application incomprehensible.
Onboarding a dataset was its own engineering workflow
The phrase “import the data” concealed a surprising amount of work. Every provider or authority brought its own conventions: workbook layouts, local asset identifiers, street abbreviations, road-pair descriptions, direction suffixes, coordinates of varying quality and missing fields. A parser that worked for one register could confidently misread the next.
We built source-specific importers first, then shared the mechanics that actually proved common. Each adapter preserved the source identity, original values, reporting context and reconciliation status. Re-importing a source needed to replace that source's snapshot predictably, without erasing unrelated contributions or making duplicate assets.
Matching was a separate stage. We normalised road names and aliases, compared road pairs, used fuzzy name matching and geographic evidence, and ranked possible links to OSM junctions. Several source assets could legitimately occupy one physical location. An unmatched row still belonged in the inventory; lack of a reliable position was a fact to retain, not a reason to discard it.
The difficult cases brought us back to the browser. We inspected the application and used Google Maps during the documented location-review workflow to check confusing road names and possible junctions. That helped identify candidates, but a plausible visual match did not establish equipment ownership or turn an agent-assisted review into human confirmation. Google Maps was a review aid, not a source we could freely copy into OSM or redistribute. Third-party sources retain their own terms, and OSM explicitly warns against adding copyrighted source data without permission. OSM source and licensing guidance.

The workflow preserves uncertain and unpositioned records. A candidate link and an accepted relationship are different states.
In one September review, Claude challenged the distinction between an OSM-derived location hint and a canonical junction identity. During browser verification, Codex also found that the UI could expose an old fuzzy candidate after the assisted workflow had selected a different target. Codex corrected the data and consumer boundaries, and verified the resulting dossiers in the browser. This was exactly where the collaboration earned its place: the importer could look correct in isolation while the person clicking the map still received the wrong story.
Making the map feel usable took several tuning cycles
My early mental model was that a slow map meant too many things were being drawn. Sometimes that was true. The first prototype needed bounded visible objects, clustering, coordinate reuse and repairs to its initial paint. Moving to MapLibre and PMTiles gave us a better basis for exploring real geography, but did not finish the performance work.
By September I was judging the experience with a mouse: drag, zoom, change a filter, revisit an area, and inspect a junction without losing my place. We profiled each stage rather than calling every delay “rendering”. Request acquisition, evidence preparation, network transfer, browser source updates and the renderer's worker/GPU work are different costs.
One tuning cycle found that changing labels and panels changed the map's dimensions. The client could query old bounds, then resize and request again. We reserved space for changing content, synchronised the final camera dimensions before querying and skipped unchanged resize calls. The initial city focus then used the intended discovery request followed by the final viewport request.
Another cycle found that filter actions were clearing useful in-memory viewport results. We preserved short-lived entries under complete context keys, while retaining expiry and invalidation for edits and access changes. In the recorded desktop browser run, 30 primed filter changes produced 30 cache hits, zero new requests and zero aborts, with exact visible-count and source-input parity. Median application completion was 90.5 ms, with 116.5 ms at p95. That timing used coarse polling; it was not a GPU paint measurement or a universal sub-100 ms guarantee.
The server still mattered on a cache miss. Spatial pruning and moving selection ahead of expensive enrichment reduced work without changing the answer. In the 19 September hosted comparison, the street-scale Cape Town viewport median fell from 771 ms to 529 ms across 30 requests per arm. The overview median went the other way, from 1,596 ms to 1,687 ms. Those sequential observations include host and I/O variation, so I cannot attribute every millisecond to one code change or claim that every navigation became faster.
The lesson was cumulative: reduce unnecessary drawing, stop accidental repeat requests, reuse only the right fresh evidence, then improve the miss path. Each cycle had to preserve the junction identities, filters, visible counts, dossiers and exports. A quick map showing stale or incomplete evidence would have failed the experiment.
September: preserving what people had learned
Once colleagues could contribute information, a data refresh became a more serious event.
OpenStreetMap changes. Grouping logic improves. A set of source nodes that once represented one candidate may later split or join another group. If the next build simply minted new identities, human observations could become detached from the places they described.
Stable identity and reconciliation became central to the system. We retained source identities, tracked aliases and treated ambiguous changes conservatively. The September junction-accuracy work repeatedly tested whether refreshed geography preserved the facts and review decisions already accepted by people.
One correction in that history is especially revealing: preservation had to include deleted record heads as well as the active records returned by the API. Looking only at what was currently visible would miss part of the committed history.
The write model follows the same principle. A contribution creates an event, and a conditional update advances the record's current head. If two users edit the same version, the stale write conflicts instead of silently overwriting the newer one. The accepted history follows the committed ancestry. A failed competing write does not become an accepted edit just because an event file exists.
Immutable geographic releases and mutable human evidence therefore live behind separate storage boundaries. Publishing a new map, or rolling back an application release, must preserve contribution history.
I had started with the desire to click on an intersection. I was now thinking about identity, concurrency and the durability of institutional knowledge.
The software architecture we arrived at
Intersection Intelligence lives in the same repository as the wider platform, but runs as a standalone application. It reuses the platform's identity and AI capabilities without absorbing the financial application's runtime or its data model.

Software architecture, simplified for public discussion. Arrows show the main request and evidence paths. Product-specific datasets and deployment identifiers are omitted.
The map interface is a lean JavaScript application using MapLibre and PMTiles. It requests bounded viewports and opens detailed evidence when needed. The shared Signal AI chat client brings its React-based conversation interface into that native map host.
A Node.js/Fastify service owns the product API. Shared domain code supplies the geographic and evidence semantics used across views. Prepared files support map and dossier reads. Read-only SQLite query workers support analytical requests. A separate mutable repository stores contributions, review state and their history.
The offline preparation pipeline uses Python geospatial tooling to work through OpenStreetMap extracts and other approved sources. It builds the territorial hierarchy, junction and corridor artifacts, indexes and query stores. Source-specific adapters retain provenance rather than assuming every authority's spreadsheet is the same format.

The concrete technology stack, separate from the request-flow architecture above. Python prepares geography; JavaScript serves and presents it. Shared AI and identity remain explicit integrations.
The fact that the software sits in one repository does not remove these boundaries. The deployed package must contain what its runtime imports. A test passing because a sibling package happens to exist in the checkout is insufficient evidence that the isolated service will start.
This architecture also gave us a useful way to expand. The country catalogue describes administrative structures instead of hardcoding every place into a South African province-and-municipality hierarchy. By late October, the published catalogue covered nine countries across southern Africa and the United Kingdom. That is geographic breadth, with different levels of source coverage in different places. It is not a claim of complete infrastructure knowledge in any of them.
Deployment became part of the design
There are two distinct release activities. An application change packages and deploys code. A geography change prepares, validates and promotes a data release. The second is an explicit operator workflow, separate from ordinary application CI.

Deployment architecture as documented in early November 2026. Cloud resource names, hostnames, network ranges, account identifiers, commercial data and operational credentials are intentionally omitted.
The research application runs on one Azure App Service Linux B2 instance, with two cores and 3.5 GB RAM, the built-in Node.js 24 LTS runtime and Always On. Microsoft's published B2 plan lists 10 GB of plan storage; that quota is distinct from the worker's temporary local disk, whose actual headroom we measured for query copies. These are configuration details, not a pricing or capacity promise. Azure App Service Linux plan specifications.
That modest memory envelope shaped the design. Heavy extract processing runs elsewhere. The serving process reads prepared artifacts, bounds query work and keeps recoverable SQLite copies on local disk. Local disk can disappear when a worker is replaced, so verified persistent releases remain the source of recovery. The host is protected by Microsoft Entra authentication. It calls the existing platform API for entitlements and the shared AI service for AI execution. Prepared immutable application datasets reside on persistent application storage, with verified local SQLite copies for ordinary analytical execution.
Basemap archives now occupy a separate storage account. Authorised browsers receive short-lived, read-only signed access to geographic tile ranges. Restricted source evidence remains behind the application, and its storage network access is restricted to the application's integration subnet. Ownership overlays follow the authenticated application path rather than being treated as ordinary basemap content.
The network design uses a storage service endpoint and a restricted public endpoint. It does not use Private Link. That distinction matters when explaining what we built, even in a diagram with all the resource names removed.
Releases also became more selective. A UI-only change should not copy the map dataset or restart the analytical worker. An application release should retain the active data root. A data release needs a validated candidate and a verified recovery path. The release procedure checks what is actually running, rather than treating a successful upload as proof of completion.
There are still operational limits. After the November storage separation, data-release and ownership-publication maintenance remain gated pending the reviewed access procedure. Ordinary application and UI releases remain supported. That is an open engineering task, not something I want a simplified architecture picture to conceal.
The next performance cycle moved into analytical queries
The map also had to coexist with analytical requests. Improving browser navigation would achieve little if a large query repeatedly reopened stores or prepared the same evidence on that small worker.
We profiled the path from the user's action through acquisition, preparation, query execution and rendering. The work led to bounded viewport caches, reuse of freshly validated evidence, local query stores and supervised resident SQLite workers. We moved filtering ahead of expensive enrichment where the semantics allowed it.
The important qualification is “where the semantics allowed it.” Filtering early is only an improvement if it preserves the population the user asked about. Pagination must not turn a partial page into an apparent total. Caching must retain freshness and invalidate when relevant evidence changes.
One small production comparison captures both the value and the limits. In the 22 September resident-worker experiment, the median of three repeated provider-count requests fell from 855 ms to 276 ms. The first request was essentially unchanged, and other query shapes improved by much less. The same comparison checked result equality and export parity.
That is a useful improvement in a measured path. It does not establish a threefold improvement in the whole application, or prove sustained multi-user capacity. The project records keep those acceptance questions open.
I have learned to be suspicious of performance stories that only show the fastest inner function. What matters is whether the person using the map gets the same answer, with the right evidence, sooner.
AI helped build the experiment, then became part of it
These are two different roles for AI in this story.
During development, Codex did sustained implementation and verification work. Claude served as a peer architect and independent reviewer in recorded collaboration sessions. Inside the application, Signal AI answers questions through authorised, read-only tools over the application's evidence.
I had already written about the development arrangement in Two AIs, One Branch, No Human Clipboard. For Intersection Intelligence, the same working pattern became a way to explore a new technical domain while retaining a clear owner for the implementation.

The collaboration pattern used in the recorded development sessions. Roles varied in separate workstreams; this does not imply that every commit received independent review.
Codex and Claude could examine the same repository and discuss bounded decisions without me copying every response between applications. The durable record was a curated engineering ledger: the question, the evidence, the finding, the correction and the remaining decision.
The independent review was useful because it sometimes changed the design. The early reconciliation explicitly retired obsolete assumptions about the map renderer and running heavy extraction on the serving host. Later AI-platform reviews challenged financial-context assumptions, conversation handling and the boundary between a client timeout and work that was still consuming server resources.
Agreement between two models was never sufficient proof. Codex still had to run the checks and produce execution evidence. A source review did not magically become a browser test, and an approved design did not mean the feature was deployed.
I also had to manage the process. The late-September AI integration grew into a long refactor and review exercise. I pushed for a first usable release, separated unrelated boundary work from that delivery, and eventually asked Codex to continue without the reviewer. The history preserves that change of responsibility. It would be misleading to describe the subsequent work as independently approved by Claude.
The agents made unfamiliar implementation work much more accessible to me. They did not remove the need to set scope, challenge complexity or decide when the next useful increment should reach a user.
Giving the map a conversational interface
The existing platform already had Signal AI. Bringing it into Intersection Intelligence prompted a broader question: which parts should every future application be able to reuse?
The answer included both the engine and the interface. We separated the shared conversation client, authenticated execution and central operations from product-owned tools and context. Intersection Intelligence owns the meaning of its geography and evidence. The financial application retains the meaning of its financial tools.
A selected map area cannot be represented by a made-up business unit just to satisfy an existing financial gate. Likewise, “here” in a saved conversation must retain the area that was selected for that turn. Panning the map later must not silently change what an earlier question meant.
The model receives bounded evidence through the product's read tools. Counts and calculated comparisons come from deterministic code. The answer must distinguish complete results, partial pages, unknown fields and unavailable evidence. The model helps explain those facts in conversation.
This is also why I care about reusable chat UX. People should not have to learn a different conversation panel for every application. The map keeps its own navigation and space, while the shared client supplies familiar conversation controls and rich answer rendering.
The first native AI release reached production at the end of September under an explicitly bounded launch decision. Answer quality, latency and capacity continued to need work. Reuse gave us a foundation for improving that experience across applications.
What I would carry into the next unfamiliar stack
I would start again with a concrete user question and a narrow, real-data slice. The demonstration dataset helped us prove interfaces. Real geography exposed the work those interfaces actually needed to do.
I would establish evidence and identity rules earlier than feels necessary. A map is persuasive. That makes it especially important to explain what its colours, counts and points mean before people depend on them.
I would keep the build pipeline separate from the serving path, and measure the actual user journey early. I would also keep the distinction between implementation, deployment and acceptance visible throughout the project. Those are different achievements.
And I would keep using AI as an engineering collaborator. The productive loop was specific: explain the intent, inspect the existing system, propose a bounded change, implement it, challenge it with evidence, and check the result a user would actually receive. Long conversations alone were never the measure of progress.
Today I can navigate to a place, inspect its evidence, preserve an attributable correction and ask questions in context. There is still more to do on coverage, richer topology, performance and validation of future analytical capabilities.
I began without experience building mapping applications. I now have a working geospatial research application and a much better understanding of the questions underneath one. The most valuable change is that I can look at an unfamiliar stack and begin with a useful problem, knowing that I can work through the engineering with AI while remaining responsible for the result.
Returning to the vision: what counted as an acceptable MVP?
The original vision imagined a rich digital representation of intersections and their wider road network. It included questions about responsibility, equipment, corridors and future planning. The requirements were useful as a direction, but building every ambition before trying the map would have postponed the learning I wanted.
The acceptable MVP became a discovery-and-capture research alpha: follow a place from the map to its evidence, preserve a reviewed correction, and ask a bounded question in context. Here is how I now compare the vision with the delivered slice.
| Original requirement or ambition | Accepted research MVP | Still outside that acceptance |
|---|---|---|
| Navigate from a country to a useful local place | Country, area, junction and published corridor navigation | Complete, equally detailed coverage everywhere |
| Bring disparate source records together | Source-specific imports, provenance and explicit candidate links | Automatic certainty for ambiguous matches |
| Build a durable shared picture | Dossiers, attributable contributions, review and preserved identities | A complete, continuously current national twin |
| Ask questions about what is on screen | Contextual AI using authorised read tools and deterministic facts | Unbounded answers or predictions unsupported by data |
| Make exploration usable on modest hosting | Measured navigation and query improvements on one B2 host | A universal latency target or sustained multi-user capacity claim |
| Explore network and planning possibilities | Published corridors and evidence for further research | Live control, calibrated traffic simulation or proven intervention benefits |
That was a deliberate stopping point for a useful first experiment, not a claim that the entire vision had shipped. Coverage, richer topology, AI quality, sustained capacity and the gated data-maintenance procedure remain work to do.
For me, the result is already meaningful. I can move from a country map to a road, inspect why a record is there, and see where the evidence runs out. I learned how the stack fits together by building it with Codex and Claude, testing the awkward cases and making the acceptance decisions myself. The project remains my personal learning experiment and an internal research tool, with no commercial use.
Technology and further reading
The stack combines MapLibre GL JS, PMTiles and Protomaps, OpenStreetMap extracts, Python geospatial preparation, Node.js/Fastify, SQLite, Azure App Service and Blob Storage, Microsoft Entra authentication, and the shared Signal AI platform.
OpenStreetMap is a community-built data source available under the ODbL, including for commercial use subject to its terms. Calling this project research does not remove attribution, licence or applicable share-alike obligations. Data licensing and access to OSM-operated tile services are separate matters; this application uses prepared PMTiles basemaps. See OpenStreetMap attribution and licensing and the OSM tile service policy.
No comments:
Post a Comment