<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rust on Man-You</title><link>https://man-you.ringum.net/tags/rust/</link><description>Recent content in Rust on Man-You</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 24 Mar 2026 09:30:00 +0100</lastBuildDate><atom:link href="https://man-you.ringum.net/tags/rust/index.xml" rel="self" type="application/rss+xml"/><item><title>The most useless way to port a macOS app</title><link>https://man-you.ringum.net/posts/clone-desktop/</link><pubDate>Tue, 24 Mar 2026 09:30:00 +0100</pubDate><guid>https://man-you.ringum.net/posts/clone-desktop/</guid><description>&lt;p&gt;I grew up fascinated by projects like GNUStep, Haiku, Etoile, Wine, and ReactOS. Engineering feats, all of them. They reverse-engineer or reimplement entire operating system APIs so that software written for one platform can run on another. And they almost always end up in the same place: impressive technically, starved for contributors, forever chasing a moving target they can never quite catch.&lt;/p&gt;
&lt;p&gt;I never liked the state of the Linux desktop either. Not because it&amp;rsquo;s bad per se, but because it&amp;rsquo;s fragmented. A KDE app on GNOME looks alien. Firefox rolls its own everything. GTK and Qt will never agree on anything. Every toolkit draws its own widgets, manages its own text rendering, handles its own accessibility story. The result is a desktop that feels like a coalition of independent projects rather than a coherent system.&lt;/p&gt;
&lt;p&gt;This project is not going to fix any of that. But it&amp;rsquo;s an interesting story about how a combination of prior work, Apple open-sourcing key components, and an AI pair programmer led me down a rabbit hole I didn&amp;rsquo;t plan to enter.&lt;/p&gt;
&lt;h2 id="the-prior-art-that-made-this-possible"&gt;The prior art that made this possible&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been working with wgpu (Rust&amp;rsquo;s WebGPU implementation) for a while now, building a &lt;a href="https://man-you.ringum.net/backroom/woosmap-tiles/"&gt;map renderer&lt;/a&gt; for Woosmap. That project taught me the fundamentals: how to manage GPU pipelines, how to do instanced rendering, how to deal with text atlases and glyph rasterization, how to bridge Rust and Swift through UniFFI.&lt;/p&gt;
&lt;p&gt;On the Swift side, I had two apps: &lt;strong&gt;Tunes&lt;/strong&gt;, a music player, and &lt;strong&gt;Leela&lt;/strong&gt;, an internal management tool for Woosmap services. Both are SwiftUI apps, both depend on Apple&amp;rsquo;s frameworks in the usual way.&lt;/p&gt;
&lt;p&gt;So one evening, half-curious and half-joking, I fed Claude the source of those projects alongside some context about GNUStep and friends, and typed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;What would it take to make Tunes build and run on Linux?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then things got out of control.&lt;/p&gt;
&lt;h2 id="what-claude-came-back-with"&gt;What Claude came back with&lt;/h2&gt;
&lt;p&gt;The answer was, predictably, &amp;ldquo;a lot.&amp;rdquo; But the interesting part was the breakdown. SwiftUI is the main dependency, and SwiftUI is closed-source. But Swift itself is open-source. Swift Foundation is open-source. The Swift Package Manager works on Linux. So the gap is really:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A SwiftUI implementation (the view hierarchy, state management, layout engine, modifiers)&lt;/li&gt;
&lt;li&gt;Some AppKit shims (NSColor, NSAppearance, the bits that SwiftUI still leans on)&lt;/li&gt;
&lt;li&gt;A rendering backend that isn&amp;rsquo;t Core Animation or Metal&lt;/li&gt;
&lt;li&gt;A compositor to manage windows, menus, and the desktop chrome&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Instead of stopping at &amp;ldquo;that&amp;rsquo;s insane, don&amp;rsquo;t do it,&amp;rdquo; I kept going. Claude kept going. We started building.&lt;/p&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The project (codenamed &lt;strong&gt;Clone&lt;/strong&gt;) splits pretty naturally along a language boundary.&lt;/p&gt;
&lt;p&gt;Rust does what Rust is good at: GPU work. A wgpu renderer with pipelines for rectangles, rounded rects (SDF-based, because I can&amp;rsquo;t stop using SDFs apparently), shadows, text via a glyph atlas, and wallpapers. It also runs the window compositor through winit.&lt;/p&gt;
&lt;p&gt;Swift does what Swift is good at: UI. A from-scratch SwiftUI implementation (about 50 View types, &lt;code&gt;@State&lt;/code&gt;/&lt;code&gt;@Binding&lt;/code&gt;/&lt;code&gt;@Environment&lt;/code&gt;, &lt;code&gt;ViewBuilder&lt;/code&gt;, layout, modifiers), the whole declarative stack. Plus enough AppKit shims to keep real apps happy, a SwiftData reimplementation backed by SQLite, and an IPC protocol over Unix sockets.&lt;/p&gt;
&lt;p&gt;UniFFI bridges the two. Each frame, Rust asks Swift for render commands, Swift resolves the view tree into a flat list of positioned primitives, and Rust batches them into instanced GPU draws. It&amp;rsquo;s the same bridge I use in the map renderer, so at least that part wasn&amp;rsquo;t new territory.&lt;/p&gt;
&lt;pre class="mermaid"&gt;graph LR
A["App.body"] --&gt; B["ViewBuilder"] --&gt; C["_resolve()"] --&gt; D["Layout"]
D --&gt; E["CommandFlattener"] --&gt; F["IPC
CGFloat → Float"] --&gt; G["Rust batcher"] --&gt; H["wgpu draws"]
style A fill:#c4a7e7,stroke:#6e6a86,color:#191724
style B fill:#c4a7e7,stroke:#6e6a86,color:#191724
style C fill:#c4a7e7,stroke:#6e6a86,color:#191724
style D fill:#c4a7e7,stroke:#6e6a86,color:#191724
style E fill:#f6c177,stroke:#6e6a86,color:#191724
style F fill:#f6c177,stroke:#6e6a86,color:#191724
style G fill:#9ccfd8,stroke:#6e6a86,color:#191724
style H fill:#9ccfd8,stroke:#6e6a86,color:#191724
&lt;/pre&gt;
&lt;h2 id="first-signs-of-life"&gt;First signs of life&lt;/h2&gt;
&lt;p&gt;The moment I&amp;rsquo;ll remember is the grey rectangle. &amp;ldquo;Clone Desktop&amp;rdquo; in the center, a dock at the bottom with colored squares. I&amp;rsquo;d been at it for hours, wrestling with UniFFI bindings and layout math, and suddenly there it was: pixels on screen, drawn by Swift code, pushed through a Rust GPU backend, on something that was decidedly not macOS.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-1_hu_60da9db4b46e573c.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-1_hu_60da9db4b46e573c.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-1_hu_cd2b29062a903b83.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="The first Clone Desktop render: a grey surface, centered label, and a color-swatch dock"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;Day one: a grey box, a label, and a dock. It&amp;#39;s not much, but it compiles and renders.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="settings-and-finder"&gt;Settings and Finder&lt;/h2&gt;
&lt;p&gt;Once the basic views worked (stacks, text, lists, navigation), I needed something real to throw at them. Settings was a natural first target: sidebars, forms, toggles, text fields, the kind of layout variety that breaks things fast. Finder came next because browsing a directory with &lt;code&gt;List&lt;/code&gt; and &lt;code&gt;ForEach&lt;/code&gt; is such a fundamental SwiftUI pattern that if it didn&amp;rsquo;t work, nothing would.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-2_hu_c535fac1e06a428f.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-2_hu_c535fac1e06a428f.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-2_hu_d543a0938420ea6e.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Clone Desktop running Settings (Wi-Fi panel) and Finder side by side, dark mode"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;Settings showing Wi-Fi preferences alongside Finder browsing the home directory. Both are SwiftUI apps running through Clone&amp;#39;s stack.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Dark mode just kind of happened. Once I shimmed &lt;code&gt;NSAppearance&lt;/code&gt; and implemented the semantic color system (&lt;code&gt;Color.primary&lt;/code&gt;, &lt;code&gt;.secondary&lt;/code&gt;, the system grays), flipping between light and dark was a toggle. A small thing, but unreasonably satisfying: it made the whole experiment feel like a real desktop for the first time.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-3_hu_7656abf32f2c59d8.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-3_hu_7656abf32f2c59d8.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-3_hu_477ea4514a69efe6.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Settings showing the General panel in light mode"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;The same Settings app in light mode. Appearance switching works through the reimplemented NSAppearance and Color system.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="tunes"&gt;Tunes&lt;/h2&gt;
&lt;p&gt;This was the whole point, remember? &amp;ldquo;What would it take to make Tunes build and run on Linux?&amp;rdquo; Well, it builds. The login sheet renders inside the app window rather than as a separate &lt;code&gt;NSPanel&lt;/code&gt; (a compromise, but not a terrible one), and the SwiftUI code is essentially unchanged. &lt;code&gt;TextField&lt;/code&gt;, &lt;code&gt;SecureField&lt;/code&gt;, &lt;code&gt;Toggle&lt;/code&gt;, &lt;code&gt;Button&lt;/code&gt;, &lt;code&gt;Link&lt;/code&gt;: they all resolve and render. Same source, different universe.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-4_hu_5ff44dcb7735661f.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-4_hu_5ff44dcb7735661f.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-4_hu_b08f4d867416f64c.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Tunes showing a login form with username/password fields, a toggle, and a registration link, alongside a Finder window"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;Tunes running its login flow through Clone. The sheet renders in-window rather than as a separate panel, one of many compromises.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="leela"&gt;Leela&lt;/h2&gt;
&lt;p&gt;Leela is a management dashboard for Woosmap services: tabs, lists, nested navigation, version selectors, deploy queues. Most of the UI is driven by API responses, so it hammers &lt;code&gt;ForEach&lt;/code&gt; with dynamic data, &lt;code&gt;@StateObject&lt;/code&gt;, and conditional rendering. If Settings was a stroll through the park, Leela was a stress test.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-5_hu_2145acbb1b59b75f.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-5_hu_2145acbb1b59b75f.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-5_hu_a5c8020ac9f97b4f.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Leela showing a service management view with sidebar navigation, tab bar, version tags, and service listings"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;Leela&amp;#39;s services view running through Clone. Tabs, lists, version badges, sidebar navigation, all SwiftUI, all reimplemented.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;I won&amp;rsquo;t pretend it was smooth. Getting here meant implementing a surprising amount of SwiftUI&amp;rsquo;s surface area. The state management system alone (&lt;code&gt;StateGraph&lt;/code&gt;, scoped identity for &lt;code&gt;ForEach&lt;/code&gt;, call-index disambiguation) went through several iterations where everything would render once and then silently stop updating. The kind of bug where you stare at a diff for an hour before realizing a closure captured a copy instead of a reference.&lt;/p&gt;
&lt;h2 id="under-the-hood"&gt;Under the hood&lt;/h2&gt;
&lt;h3 id="text-rendering"&gt;Text rendering&lt;/h3&gt;
&lt;p&gt;Text goes through cosmic-text for shaping, then gets rasterized into a 4096x4096 glyph atlas (single-channel, R8). Each glyph is cached and rendered as an instanced GPU quad. Fonts are bundled: Inter for UI text, Phosphor for icons.&lt;/p&gt;
&lt;p&gt;This is almost certainly not how Apple does it. A real system would share GPU textures between the compositor and app, or pull from a system font cache. But it works, and there&amp;rsquo;s a special joy in watching a glyph atlas fill up character by character as the UI renders for the first time.&lt;/p&gt;
&lt;h3 id="layout"&gt;Layout&lt;/h3&gt;
&lt;p&gt;I reverse-engineered SwiftUI&amp;rsquo;s layout by reading &lt;a href="https://www.objc.io/books/thinking-in-swiftui/"&gt;objc.io&amp;rsquo;s&lt;/a&gt; thinking-in-swiftui and a lot of trial and error. The model: parents propose a size to children, children report back what they need, parents position them. Sounds simple until &lt;code&gt;ZStack&lt;/code&gt; enters the picture: nil-sized views from &lt;code&gt;.background()&lt;/code&gt; modifiers would expand to fill constraints and eat space from real siblings. That one took a while.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ScrollView&lt;/code&gt; was another head-scratcher: it fills its proposed size but lays out content unbounded in the scroll axis. Getting that inversion right, where the container constrains in one direction and is infinite in the other, broke my mental model twice before clicking.&lt;/p&gt;
&lt;h3 id="ipc"&gt;IPC&lt;/h3&gt;
&lt;p&gt;Each app is a separate process. The compositor (CloneDesktop) runs the Rust event loop and manages surfaces. Apps connect over a Unix socket at &lt;code&gt;/tmp/clone-compositor.sock&lt;/code&gt; and exchange length-prefixed JSON messages. There&amp;rsquo;s an annoying &lt;code&gt;CGFloat&lt;/code&gt;-to-&lt;code&gt;Float&lt;/code&gt; conversion at the wire boundary: Swift thinks in 64-bit coordinates, the GPU thinks in &lt;code&gt;f32&lt;/code&gt;, and someone has to reconcile that at the border.&lt;/p&gt;
&lt;h3 id="the-ycodebuild-trick"&gt;The &lt;code&gt;ycodebuild&lt;/code&gt; trick&lt;/h3&gt;
&lt;p&gt;This is probably my favorite hack in the project. To compile an existing macOS app against Clone instead of Apple&amp;rsquo;s frameworks, &lt;code&gt;ycodebuild&lt;/code&gt; generates a shadow SPM package that maps &lt;code&gt;import SwiftUI&lt;/code&gt; to Clone&amp;rsquo;s SwiftUI, &lt;code&gt;import AppKit&lt;/code&gt; to Clone&amp;rsquo;s shims, and so on. The app source code doesn&amp;rsquo;t change at all: you just build against a different package graph, and the compiled binary talks to the compositor over the socket. The same &lt;code&gt;.swift&lt;/code&gt; file, two completely different platforms.&lt;/p&gt;
&lt;h2 id="honest-assessment"&gt;Honest assessment&lt;/h2&gt;
&lt;p&gt;I should be upfront about the gap between &amp;ldquo;it renders&amp;rdquo; and &amp;ldquo;it works.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Animations are mostly stubbed. Accessibility is nonexistent. The compositor redraws every surface every frame like it&amp;rsquo;s 1997. There are &lt;code&gt;// TODO: implement&lt;/code&gt; scattered across the codebase where AppKit APIs return no-ops and hope nobody notices. Sheets render in-window because I never built the panel system. The glyph atlas will fall over the moment someone opens a CJK document.&lt;/p&gt;
&lt;p&gt;And (this is the awkward part) it doesn&amp;rsquo;t actually run on Linux yet. The whole premise was &amp;ldquo;what would it take,&amp;rdquo; and the answer turned out to include some Apple framework dependencies I haven&amp;rsquo;t replaced with their open-source equivalents. It&amp;rsquo;s doable. It&amp;rsquo;s just not done.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the thing that caught me off guard. The time from that initial Claude prompt to a state where Tunes and Leela compile and render recognizable UI was hours, not weeks. Not autonomous AI magic (plenty of manual fixes and architectural decisions along the way) but Claude carried an enormous amount of boilerplate: generating 50 View type stubs, wiring up modifier chains, implementing the state graph, setting up IPC. The kind of work that would&amp;rsquo;ve taken me days of tedious typing, compressed into a conversation.&lt;/p&gt;
&lt;h2 id="why-this-matters-a-little"&gt;Why this matters (a little)&lt;/h2&gt;
&lt;p&gt;Apple open-sourcing Swift and Foundation was a bigger deal than most people realize. Not because anyone&amp;rsquo;s going to ship a SwiftUI app on Linux tomorrow, but because it lowered the floor for experiments like this from &amp;ldquo;completely impossible&amp;rdquo; to &amp;ldquo;merely impractical.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The projects I admired growing up (GNUStep, Haiku, Wine) were built by small teams reverse-engineering closed systems over years. The combination of open-source language infrastructure and AI assistance compresses that timeline dramatically. Not to a point where it&amp;rsquo;s practical or production-ready, but to a point where a single person can explore the shape of the problem in a weekend.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the real takeaway. Not &amp;ldquo;I ported macOS to Linux.&amp;rdquo; I didn&amp;rsquo;t. But I went from a throwaway prompt to colored boxes on screen running real SwiftUI app code, and that felt like something worth writing about.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s next&lt;/h2&gt;
&lt;p&gt;Honestly? Probably nothing. The project scratched a twenty-year itch. But I know myself, and I know there&amp;rsquo;s a Kawase blur pipeline sitting unused in the renderer that&amp;rsquo;s going to call my name at 11pm some Tuesday. If I do keep going:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Per-window offscreen textures&lt;/strong&gt;: the compositor shouldn&amp;rsquo;t redraw everything every frame, that&amp;rsquo;s embarrassing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Glassmorphism / backdrop blur&lt;/strong&gt;: because what&amp;rsquo;s the point of reimplementing macOS if you can&amp;rsquo;t have the frosted glass&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actually running on Linux&lt;/strong&gt;: the whole original premise, still unfinished&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared GPU textures&lt;/strong&gt;: how a real compositor would work, instead of copying pixels through a socket like an animal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Or maybe it stays as a weekend experiment and a blog post. Either way, the kid who thought GNUStep was the coolest thing ever is pretty happy right now.&lt;/p&gt;</description></item><item><title>Calculon part 2: costing profiles, performance, and maneuvers</title><link>https://man-you.ringum.net/posts/calculon-part2/</link><pubDate>Wed, 18 Feb 2026 10:00:00 +0200</pubDate><guid>https://man-you.ringum.net/posts/calculon-part2/</guid><description>&lt;p&gt;&lt;figure&gt;
&lt;img src="https://man-you.ringum.net/posts/calculon-part2/calculon.svg" alt="Calculon" /&gt;
&lt;figcaption&gt;Why can&amp;#39;t I move on?&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://man-you.ringum.net/posts/calculon/"&gt;Part 1&lt;/a&gt; covered the basics: six crates, Valhalla&amp;rsquo;s tile format, a driving cost model, bidirectional A*, many-to-many matrix, and an Axum HTTP server. Everything worked for the driving case on the Monaco dataset.&lt;/p&gt;
&lt;p&gt;Since then, the engine gained bicycle and pedestrian costing, memory-mapped tiles, a comparison webapp, turn-by-turn directions in 34 languages, and enough performance work to make it usable on France-scale data. This is what changed.&lt;/p&gt;
&lt;h2 id="bicycle-and-pedestrian-costing"&gt;Bicycle and pedestrian costing&lt;/h2&gt;
&lt;p&gt;Two new cost models implement the &lt;code&gt;DynamicCost&lt;/code&gt; trait alongside the existing &lt;code&gt;AutoCost&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bicycle&lt;/strong&gt; defaults to 18 kph (hybrid bike). The model factors in grade, road stress, surface quality, and turn costs, all lighter than auto because a cyclist doesn&amp;rsquo;t mind a left turn as much as a driver crossing oncoming traffic.&lt;/p&gt;
&lt;p&gt;Grade uses a Tobler-style speed table. Flat terrain is 1.0x; steep downhill goes up to 2.2x; steep uphill drops to 0.3x. Hill avoidance is a separate preference (&lt;code&gt;use_hills&lt;/code&gt;) that applies an additional penalty on grades regardless of the speed adjustment.&lt;/p&gt;
&lt;p&gt;The interesting part is road stress, how comfortable a road is to cycle on:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-costing/src/bicycle_cost.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-costing/src/bicycle_cost.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;accommodation_factor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;DirectedEdge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="kt"&gt;f32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;use_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;use_type&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;use_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Use&lt;/span&gt;::&lt;span class="n"&gt;Cycleway&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;use_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Use&lt;/span&gt;::&lt;span class="n"&gt;MountainBike&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cycle_lane&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CycleLane&lt;/span&gt;::&lt;span class="n"&gt;Separated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CycleLane&lt;/span&gt;::&lt;span class="n"&gt;Dedicated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CycleLane&lt;/span&gt;::&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CycleLane&lt;/span&gt;::&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bike_network&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shoulder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;A separated cycle lane costs nothing extra. A shared lane adds 0.3. No accommodation at all on a road that&amp;rsquo;s not part of any bike network costs 1.0, scaled by the &lt;code&gt;use_roads&lt;/code&gt; preference. Combined with &lt;code&gt;ROAD_CLASS_FACTOR&lt;/code&gt; that penalizes trunk roads (0.8) vs residential (0.0), the engine steers cyclists toward quieter streets with bike infrastructure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pedestrian&lt;/strong&gt; defaults to 5.1 kph. It adds SAC hiking scale (7-tier trail difficulty), a step penalty for stairs, lighting preference, and use-type bonuses for footways and sidewalks:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-costing/src/pedestrian_cost.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-costing/src/pedestrian_cost.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;GRADE_SPEED_FACTOR&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// downhill: slightly faster
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// flat
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// uphill: slower
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Both profiles make a critical architectural decision: they &lt;strong&gt;disable hierarchy entirely&lt;/strong&gt;. The A* hierarchy transitions work by &amp;ldquo;jumping up&amp;rdquo; to highways as the search moves away from endpoints. That makes sense for driving: a 200km route shouldn&amp;rsquo;t expand residential streets in the middle. But for cycling and walking, the shortcut edges that power hierarchy are built for driving and would produce wrong routes. So bicycle and pedestrian stay on level 2 where all edges are accessible, trading search speed for correctness.&lt;/p&gt;
&lt;h2 id="memory-mapped-tiles"&gt;Memory-mapped tiles&lt;/h2&gt;
&lt;p&gt;The tile reader now uses &lt;code&gt;mmap(2)&lt;/code&gt; instead of heap-allocated reads:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-tiles/src/graph_tile.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-tiles/src/graph_tile.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;TileData&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Owned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Tests
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mmap&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Production
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;from_mmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mmap&lt;/span&gt;: &lt;span class="nc"&gt;Mmap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TileError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base_ll&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GraphTile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;: &lt;span class="nc"&gt;TileData&lt;/span&gt;::&lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base_ll&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;In the graph reader:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-tiles/src/graph_reader.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-tiles/src/graph_reader.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;fs&lt;/span&gt;::&lt;span class="n"&gt;File&lt;/span&gt;::&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TileError&lt;/span&gt;::&lt;span class="n"&gt;Io&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;unsafe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mmap&lt;/span&gt;::&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="n"&gt;map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TileError&lt;/span&gt;::&lt;span class="n"&gt;Io&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Arc&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GraphTile&lt;/span&gt;::&lt;span class="n"&gt;from_mmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;No heap allocation, no copy. The OS page cache handles eviction. The old application-level LRU cache became unnecessary. Tiles are cheap &lt;code&gt;Arc&lt;/code&gt; pointers to mapped pages.&lt;/p&gt;
&lt;h2 id="thread-local-tile-cache"&gt;Thread-local tile cache&lt;/h2&gt;
&lt;p&gt;Routing makes thousands of &lt;code&gt;get_tile&lt;/code&gt; calls per search. Even a &lt;code&gt;RwLock&lt;/code&gt; on a &lt;code&gt;HashMap&lt;/code&gt; adds up. The solution is a three-level cache:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-tiles/src/graph_reader.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-tiles/src/graph_reader.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="fm"&gt;thread_local!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;TL_CACHE&lt;/span&gt;: &lt;span class="nc"&gt;RefCell&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FxHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GraphTile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RefCell&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FxHashMap&lt;/span&gt;::&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;GraphId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GraphTile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TileError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tile_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tile_value&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Level 1: thread-local (no synchronization, ~20ns)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;TL_CACHE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;borrow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tile_key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;cloned&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Level 2: shared cache (RwLock, parallel readers)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ... insert into thread-local on hit
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Level 3: mmap from disk, populate both caches
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;The hot path (thread-local hit) is a plain &lt;code&gt;HashMap&lt;/code&gt; lookup in a &lt;code&gt;RefCell&lt;/code&gt;. No lock, no contention, no &lt;code&gt;Arc::clone&lt;/code&gt; from the shared cache. FxHash (from &lt;code&gt;rustc_hash&lt;/code&gt;) replaces &lt;code&gt;std::HashMap&lt;/code&gt; everywhere, optimized for integer keys that don&amp;rsquo;t need cryptographic hashing.&lt;/p&gt;
&lt;h2 id="distance-approximator"&gt;Distance approximator&lt;/h2&gt;
&lt;p&gt;Edge snapping calls distance computations in a tight loop, projecting a point onto every nearby road segment. Haversine uses &lt;code&gt;sin&lt;/code&gt;, &lt;code&gt;cos&lt;/code&gt;, &lt;code&gt;asin&lt;/code&gt;, &lt;code&gt;sqrt&lt;/code&gt; per call. For relative comparisons within a small area, that&amp;rsquo;s overkill.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;DistanceApproximator&lt;/code&gt; pre-computes &lt;code&gt;cos(lat)&lt;/code&gt; once and then uses only multiplications:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-core/src/geo.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-core/src/geo.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;DistanceApproximator&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lng_scale&lt;/span&gt;: &lt;span class="kt"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meters_per_lng_degree&lt;/span&gt;: &lt;span class="kt"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DistanceApproximator&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;PointLL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;Self&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lng_scale&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;lng_scale_per_lat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lng_scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meters_per_lng_degree&lt;/span&gt;: &lt;span class="nc"&gt;lng_scale&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;METERS_PER_DEGREE_LAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;distance_squared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;PointLL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="kt"&gt;f64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lat_m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;center_lat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;METERS_PER_DEGREE_LAT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lng_m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lng&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;center_lng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meters_per_lng_degree&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lat_m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lat_m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lng_m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lng_m&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Construct one for the search point, reuse it across all segment projections. 26% faster in edge snapping benchmarks. The accuracy loss is negligible within a few kilometers, well within the range of any snap search.&lt;/p&gt;
&lt;h2 id="distance-adaptive-hierarchy"&gt;Distance-adaptive hierarchy&lt;/h2&gt;
&lt;p&gt;Short urban routes were sometimes jumping to highways unnecessarily. A 2km trip through a city shouldn&amp;rsquo;t consider the nearby motorway just because the hierarchy said &amp;ldquo;expand to level 0 after 5,000 meters.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The fix scales hierarchy limits proportionally to route distance:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-route/src/bidirectional_astar.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-route/src/bidirectional_astar.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;adjust_limits_for_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limits&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HierarchyLimits&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;: &lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 5km route: expand_within_dist = 2500m, max_up_transitions = 11
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 1km route: expand_within_dist = 2000m, max_up_transitions = 5
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 20km+ route: no adjustment
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;A 5km route caps level-2 expansion at 2,500m and allows only 11 upward transitions (from 100 default). A 1km route drops to 5 transitions with a 2,000m cap. Routes above 15km keep the defaults. The search still finds the optimal path, it just doesn&amp;rsquo;t waste time on irrelevant highway segments for short trips.&lt;/p&gt;
&lt;h2 id="a-heuristics-in-the-cost-matrix"&gt;A* heuristics in the cost matrix&lt;/h2&gt;
&lt;p&gt;The matrix previously used pure Dijkstra for each source/target pair: expand uniformly in all directions. Now each gets an A* heuristic aimed at its closest counterpart:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-matrix/src/cost_matrix.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-matrix/src/cost_matrix.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;closest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;da&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;da&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partial_cmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;cmp&lt;/span&gt;::&lt;span class="n"&gt;Ordering&lt;/span&gt;::&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cost_factor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heuristics_forward&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;This steers each search toward likely connection points rather than expanding uniformly. Combined with Valhalla-style hierarchy and a &lt;code&gt;path_dist_threshold&lt;/code&gt; set to 2x the max source-target distance, the matrix handles 9x9 urban grids without exploring the entire region.&lt;/p&gt;
&lt;h2 id="turn-by-turn-maneuvers"&gt;Turn-by-turn maneuvers&lt;/h2&gt;
&lt;p&gt;The first version returned routes with &amp;ldquo;Start&amp;rdquo; and &amp;ldquo;Arrive&amp;rdquo; stubs. Now there&amp;rsquo;s a proper maneuver builder that groups consecutive edges into instructions.&lt;/p&gt;
&lt;p&gt;The builder triggers a new maneuver on name changes, significant turns, road type transitions (roundabouts, ramps, ferries):&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon/src/maneuver_builder.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon/src/maneuver_builder.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;should_start_new_maneuver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in_roundabout&lt;/span&gt;: &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entering_roundabout&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exiting_roundabout&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entering_ramp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exiting_ramp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entering_ferry&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exiting_ferry&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;turn_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TurnType&lt;/span&gt;::&lt;span class="n"&gt;Reverse&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name_changed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;in_roundabout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;turn_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TurnType&lt;/span&gt;::&lt;span class="n"&gt;Straight&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name_changed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Narrative generation uses &lt;code&gt;rust-i18n&lt;/code&gt; with 34 locale files. Each maneuver type maps to a template:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;locales/en-US.yml&lt;/figcaption&gt;
&lt;div class="highlight" title="locales/en-US.yml"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;start.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Drive %{cardinal_direction} on %{street_names}.&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;turn.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Turn %{relative_direction} onto %{street_names}.&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;enter_roundabout.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Enter the roundabout and take the %{ordinal_value} exit onto %{roundabout_exit_street_names}.&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;destination.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;You have arrived at your destination.&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Roundabouts track exit counts. Cardinal directions derive from heading. Ordinals support up to the 10th exit. The locale files include French, German, Spanish, Italian, and (yes) pirate English.&lt;/p&gt;
&lt;h2 id="edge-snapping-improvements"&gt;Edge snapping improvements&lt;/h2&gt;
&lt;p&gt;Snapping a coordinate to the road network was one of the weaker points. Two fixes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-tile search.&lt;/strong&gt; When a point falls near a tile boundary, the search now checks neighboring tiles in all 8 directions:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-route/src/bidirectional_astar.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-route/src/bidirectional_astar.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;near_south&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_pt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;TILE_BOUNDARY_MARGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;near_north&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_pt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;TILE_BOUNDARY_MARGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ... check all 8 directions
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dlng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dlat&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbor_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TileHierarchy&lt;/span&gt;::&lt;span class="n"&gt;get_graph_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;neighbor_pt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;neighbor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_closest_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;access_mask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Smarter candidate pruning.&lt;/strong&gt; Post-collection, candidates beyond &lt;code&gt;min_distance * 3.0&lt;/code&gt; (minimum 25m threshold) are pruned, then &lt;code&gt;select_nth_unstable_by&lt;/code&gt; does an O(N) partition instead of a full sort. Only the top K candidates get sorted.&lt;/p&gt;
&lt;h2 id="all-my-circuits"&gt;All My Circuits&lt;/h2&gt;
&lt;p&gt;A Lit + TypeScript + MapLibre GL webapp for side-by-side comparison with Valhalla. Route and matrix modes, all three costing profiles, Valhalla comparison with percentage diffs on time and distance, resizable sidebar, URL state persistence, clickable matrix routes in verbose mode, and links to Google Maps and HERE for sanity checking.&lt;/p&gt;
&lt;p&gt;The debug mode loads per-edge data (speed, density, road class, costs) so you can see exactly why the engine chose one road over another. The inspect endpoint visualizes tile-level node and edge data for when things really go wrong.&lt;/p&gt;
&lt;h2 id="criterion-benchmarks"&gt;Criterion benchmarks&lt;/h2&gt;
&lt;p&gt;Benchmarks run against France tiles across four crates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tiles&lt;/strong&gt;: cache hit (~20ns thread-local), cold load (mmap), edge traversal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Costing&lt;/strong&gt;: single edge cost, transition cost, access check&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routing&lt;/strong&gt;: 5km urban, 55km regional, 240km long-distance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Matrix&lt;/strong&gt;: 1x1, 4x4 coastal, 9x9 urban&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A separate bench runner does HTTP-level comparison against Valhalla, reporting p50/p95/p99 latencies and route cost/distance diffs. Useful for regression testing and for validating that the cost model matches Valhalla&amp;rsquo;s output within acceptable margins.&lt;/p&gt;
&lt;h2 id="where-it-stands"&gt;Where it stands&lt;/h2&gt;
&lt;p&gt;The engine handles France-scale data with three costing profiles. Routes match Valhalla on the scenarios I&amp;rsquo;ve tested. Turn-by-turn directions work in 34 languages. The matrix scales to 9x9 with A* heuristics. Performance is good enough for a single-server deployment.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s new since part 1:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bicycle and pedestrian cost models&lt;/li&gt;
&lt;li&gt;Memory-mapped tiles with thread-local cache&lt;/li&gt;
&lt;li&gt;FxHash everywhere, DistanceApproximator for snapping (26% faster)&lt;/li&gt;
&lt;li&gt;Distance-adaptive hierarchy limits&lt;/li&gt;
&lt;li&gt;A* heuristics in the cost matrix&lt;/li&gt;
&lt;li&gt;Turn-by-turn maneuvers with localized narrative in 34 languages&lt;/li&gt;
&lt;li&gt;Multi-tile edge snapping&lt;/li&gt;
&lt;li&gt;Criterion benchmarks and a Valhalla comparison webapp&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What&amp;rsquo;s still ahead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isochrones&lt;/strong&gt;: reachability polygons from a point within a time/distance budget&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contraction hierarchies&lt;/strong&gt;: the hierarchy transitions help but true CH preprocessing would unlock cross-Europe routes at acceptable latency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Via points&lt;/strong&gt;: multi-leg routes with intermediate stops&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Building a routing engine in Rust</title><link>https://man-you.ringum.net/posts/calculon/</link><pubDate>Sat, 14 Feb 2026 10:00:00 +0200</pubDate><guid>https://man-you.ringum.net/posts/calculon/</guid><description>&lt;p&gt;&lt;figure&gt;
&lt;img src="https://man-you.ringum.net/posts/calculon/calculon.svg" alt="Calculon" /&gt;
&lt;figcaption&gt;Why can&amp;#39;t I move on?&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;A routing engine in Rust. Bidirectional A*, many-to-many distance matrices, Valhalla-compatible API. Six crates, about 8,000 lines.&lt;/p&gt;
&lt;h2 id="why"&gt;Why&lt;/h2&gt;
&lt;p&gt;We use Valhalla for routing and distance matrices at Woosmap. It works most of the time. When it doesn&amp;rsquo;t, we&amp;rsquo;re stuck. We treat it as a black box: file an issue, upgrade, hope the next release fixes things. When the matrix returns wrong costs for certain edge cases or the engine routes you through someone&amp;rsquo;s driveway, there&amp;rsquo;s no realistic way for us to dig in. It&amp;rsquo;s a massive C++ codebase that assumes you&amp;rsquo;ve been living in it for years.&lt;/p&gt;
&lt;p&gt;The goal is to replace Valhalla entirely for our use case: routing, distance matrices, and isochrones, with driving, cycling, and walking profiles. Not fork it, not wrap it. Rewrite the parts we need in Rust so we can actually own the thing.&lt;/p&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;p&gt;Six crates in a Cargo workspace:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;calculon/
├── calculon-core # Foundation types: coordinates, graph IDs, constants
├── calculon-tiles # Binary tile reader: nodes, edges, transitions
├── calculon-costing # Cost models: auto/driving with turn penalties
├── calculon-route # Bidirectional A* pathfinding
├── calculon-matrix # Many-to-many distance matrix
└── calculon # HTTP server: Axum handlers, Valhalla-compatible API&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;The HTTP layer at the top is thin: parse JSON, call the algorithm, format the response. Everything interesting happens in the middle crates.&lt;/p&gt;
&lt;h2 id="tile-based-graph"&gt;Tile-based graph&lt;/h2&gt;
&lt;p&gt;The road network lives in binary tiles, same format Valhalla uses. Nodes are intersections, directed edges are road segments, edge info holds geometry and names, node transitions connect tiles to each other.&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-tiles/src/graph_tile.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-tiles/src/graph_tile.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;from_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TileError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;size_of&lt;/span&gt;::&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GraphTileHeader&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TileError&lt;/span&gt;::&lt;span class="n"&gt;TooSmall&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;: &lt;span class="nc"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;: &lt;span class="nc"&gt;size_of&lt;/span&gt;::&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GraphTileHeader&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;unsafe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GraphTileHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_offset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;usize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TileError&lt;/span&gt;::&lt;span class="n"&gt;SizeMismatch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;header_size&lt;/span&gt;: &lt;span class="nc"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_offset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;actual_size&lt;/span&gt;: &lt;span class="nc"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base_ll&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_ll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GraphTile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base_ll&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;/// Get the node at a given index.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;: &lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;NodeInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;nodecount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;size_of&lt;/span&gt;::&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GraphTileHeader&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;usize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;size_of&lt;/span&gt;::&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NodeInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;unsafe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;as_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NodeInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;No deserialization, just pointer casting into the raw bytes. The tile is a flat buffer with a header followed by fixed-size node and edge records at known offsets. Fast, but the crate refuses to compile on big-endian targets because the byte layout has to match.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GraphReader&lt;/code&gt; sits on top of an LRU cache. Algorithm needs an edge in some tile? Check the cache, load the file if it&amp;rsquo;s not there, hand back an &lt;code&gt;Arc&amp;lt;GraphTile&amp;gt;&lt;/code&gt;. Default cache holds 200 tiles, which covers most regional queries without touching disk twice.&lt;/p&gt;
&lt;p&gt;Reusing Valhalla&amp;rsquo;s tile format was a big shortcut. Designing a graph format from scratch would have been a project in itself.&lt;/p&gt;
&lt;h2 id="cost-model"&gt;Cost model&lt;/h2&gt;
&lt;p&gt;This is where routing gets interesting, and where most bugs hide. The cost model decides how expensive each road segment is to traverse. Not just distance. Time, comfort, how annoying a particular turn is. The values here are derived from Valhalla&amp;rsquo;s own cost model. No point reinventing what they&amp;rsquo;ve already calibrated.&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-costing/src/auto_cost.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-costing/src/auto_cost.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;RIGHT_SIDE_TURN_COSTS&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Straight
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SlightRight
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Right (favorable)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SharpRight (favorable-sharp)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Reverse
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SharpLeft (unfavorable-sharp)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Left (unfavorable, crosses oncoming traffic)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SlightLeft
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;LEFT_SIDE_TURN_COSTS&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Straight
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SlightRight
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Right (unfavorable)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SharpRight (unfavorable-sharp)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Reverse
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SharpLeft (favorable-sharp)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Left (favorable)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SlightLeft
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Two tables: one for right-side driving countries (France, US), one for left-side (UK, Japan). In France, a left turn crosses oncoming traffic so it costs more. In the UK, it&amp;rsquo;s the right turn. Get this wrong and the engine starts suggesting weird U-turn loops to avoid a simple left.&lt;/p&gt;
&lt;p&gt;The rest of the cost model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Speed factors: pre-computed &lt;code&gt;3.6 / speed_kph&lt;/code&gt; lookup table, seconds per meter at each speed&lt;/li&gt;
&lt;li&gt;Highway preference: a slider from &lt;code&gt;-1.0&lt;/code&gt; to &lt;code&gt;1.0&lt;/code&gt;, push it high and the engine prefers motorways&lt;/li&gt;
&lt;li&gt;Surface penalties: gravel costs more than asphalt&lt;/li&gt;
&lt;li&gt;Intersection density: dense urban intersections add delay, factors range from 1.0 up to 3.5&lt;/li&gt;
&lt;li&gt;Toll booths: flat time penalty per crossing&lt;/li&gt;
&lt;li&gt;Traffic signals: extra delay at signalized intersections&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this is pre-computed into lookup tables at startup. During search, computing the cost of an edge is just array indexing.&lt;/p&gt;
&lt;p&gt;The algorithm finds &lt;em&gt;a&lt;/em&gt; shortest path. The cost model decides whether that path makes any sense to a human driver. Same constants Valhalla uses, but now in code we can read and step through when something looks off.&lt;/p&gt;
&lt;h2 id="bidirectional-a"&gt;Bidirectional A*&lt;/h2&gt;
&lt;p&gt;Two search trees grow at the same time: one forward from the origin, one backward from the destination. They meet somewhere in the middle.&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-route/src/bidirectional_astar.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-route/src/bidirectional_astar.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;BidirectionalAStar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edgelabels_forward&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BDEdgeLabel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edgelabels_reverse&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BDEdgeLabel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;adjacencylist_forward&lt;/span&gt;: &lt;span class="nc"&gt;DoubleBucketQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;adjacencylist_reverse&lt;/span&gt;: &lt;span class="nc"&gt;DoubleBucketQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edgestatus_forward&lt;/span&gt;: &lt;span class="nc"&gt;EdgeStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edgestatus_reverse&lt;/span&gt;: &lt;span class="nc"&gt;EdgeStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;heuristic_forward&lt;/span&gt;: &lt;span class="nc"&gt;AStarHeuristic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;heuristic_reverse&lt;/span&gt;: &lt;span class="nc"&gt;AStarHeuristic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cost_diff&lt;/span&gt;: &lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cost_threshold&lt;/span&gt;: &lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;best_connections&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CandidateConnection&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Everything is doubled: edge labels, priority queues, edge status, heuristics. The heuristic is geographic distance to the opposite endpoint. Simple, admissible, good enough.&lt;/p&gt;
&lt;p&gt;The priority queue is a double-bucket structure: coarse outer buckets, fine inner buckets. Turns out a binary heap isn&amp;rsquo;t great for routing because the cost values cluster in narrow ranges. Buckets handle that better.&lt;/p&gt;
&lt;p&gt;The search stops when the best connection found so far, plus a 420-second buffer, exceeds the cheapest item in either queue. That buffer lets it keep looking for slightly better paths after the first connection without searching forever.&lt;/p&gt;
&lt;p&gt;Hierarchy transitions help a lot. Near the endpoints, the search considers local roads. As it moves away, it &amp;ldquo;jumps up&amp;rdquo; to highways and trunk roads. In the middle of a long route, there&amp;rsquo;s no point expanding residential streets. This cuts the number of edges explored dramatically.&lt;/p&gt;
&lt;h2 id="many-to-many-matrix"&gt;Many-to-many matrix&lt;/h2&gt;
&lt;p&gt;A 50x50 distance matrix done naively (individual route for each pair) means 2,500 searches. That&amp;rsquo;s not going to work.&lt;/p&gt;
&lt;p&gt;The trick: expand all sources forward simultaneously, all targets backward simultaneously, and watch for collisions. When source &lt;code&gt;i&lt;/code&gt;&amp;rsquo;s forward search reaches an edge that target &lt;code&gt;j&lt;/code&gt;&amp;rsquo;s reverse search already touched, that&amp;rsquo;s a connection.&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon-matrix/src/cost_matrix.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon-matrix/src/cost_matrix.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;CostMatrix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edgelabels_forward&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BDEdgeLabel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;queues_forward&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DoubleBucketQueue&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edgestatus_forward&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EdgeStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edgelabels_reverse&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BDEdgeLabel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Each source and target gets its own labels, queue, and status set, but they all walk the same graph. A &lt;code&gt;ReachedMap&lt;/code&gt; (basically a &lt;code&gt;HashMap&amp;lt;edge_id, Vec&amp;lt;location_index&amp;gt;&amp;gt;&lt;/code&gt;) tracks who&amp;rsquo;s been where. When a forward expansion hits an edge in the reverse map, we record the connection cost.&lt;/p&gt;
&lt;p&gt;Result is an N*M grid of time/distance values. Some cells might be unreachable. The whole thing caps at 2 million iterations so it doesn&amp;rsquo;t run forever on disconnected graphs.&lt;/p&gt;
&lt;h2 id="http-server"&gt;HTTP server&lt;/h2&gt;
&lt;p&gt;Three endpoints on Axum, matching Valhalla&amp;rsquo;s JSON format so existing clients can point at Calculon without code changes.&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;crates/calculon/src/server.rs&lt;/figcaption&gt;
&lt;div class="highlight" title="crates/calculon/src/server.rs"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;build_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;: &lt;span class="nc"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Router&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/route&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler_route&lt;/span&gt;::&lt;span class="n"&gt;handle_route&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/sources_to_targets&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler_matrix&lt;/span&gt;::&lt;span class="n"&gt;handle_matrix&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler_status&lt;/span&gt;::&lt;span class="n"&gt;handle_status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CorsLayer&lt;/span&gt;::&lt;span class="n"&gt;permissive&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Route and matrix searches are CPU-bound: they can block for hundreds of milliseconds on a big query. They run on Tokio&amp;rsquo;s blocking thread pool via &lt;code&gt;spawn_blocking&lt;/code&gt; so they don&amp;rsquo;t starve the async runtime.&lt;/p&gt;
&lt;p&gt;Each request gets a &lt;code&gt;CancelGuard&lt;/code&gt; backed by an &lt;code&gt;AtomicBool&lt;/code&gt;. Client disconnects? The guard drops, sets the flag, and the algorithm checks it periodically and bails out. Seemed like overkill at first, but it matters when someone fires off a matrix request and immediately refreshes the page.&lt;/p&gt;
&lt;p&gt;A route request:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;locations&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;lat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;43.731&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;lon&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.419&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;lat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;43.696&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;lon&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.271&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;costing&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;auto&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;costing_options&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;auto&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;use_highways&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;units&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;kilometers&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Response comes back with a polyline6-encoded shape, total time, distance, bounding box, and some basic maneuvers (though the maneuvers are stubs for now, just &amp;ldquo;Start&amp;rdquo; and &amp;ldquo;Arrive&amp;rdquo;).&lt;/p&gt;
&lt;h2 id="where-it-stands"&gt;Where it stands&lt;/h2&gt;
&lt;p&gt;Driving works. Routes match Valhalla&amp;rsquo;s output on the Monaco dataset. The matrix handles concurrent requests with cancellation. The server drops in behind the same API.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Driving cost model with turns, highways, surface, density, tolls&lt;/li&gt;
&lt;li&gt;Bidirectional A* with hierarchy transitions&lt;/li&gt;
&lt;li&gt;Many-to-many matrix via simultaneous bidirectional Dijkstra&lt;/li&gt;
&lt;li&gt;Valhalla-compatible REST API&lt;/li&gt;
&lt;li&gt;LRU tile caching with memory-mapped I/O&lt;/li&gt;
&lt;li&gt;Cooperative request cancellation&lt;/li&gt;
&lt;li&gt;Tests against Monaco dataset&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What&amp;rsquo;s left to reach parity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cycling and walking profiles&lt;/strong&gt;: the cost model is structured for it, but only driving is implemented so far.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isochrones&lt;/strong&gt;: reachability polygons from a point within a time/distance budget. The search infrastructure is there, the polygon generation isn&amp;rsquo;t.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turn-by-turn directions&lt;/strong&gt;: maneuvers are stubs. Real maneuver generation needs street names, instruction templates, sign data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contraction hierarchies&lt;/strong&gt;: the A* is fine for regional routes but CH would be needed for cross-country at acceptable latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-leg routes&lt;/strong&gt;: only origin to destination, no via points yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;About 8,000 lines across six crates. The driving case works end to end. Cycling, walking, and isochrones are what&amp;rsquo;s between this and actually replacing Valhalla.&lt;/p&gt;</description></item><item><title>Building a GPU map renderer from scratch</title><link>https://man-you.ringum.net/posts/gpu-map-renderer/</link><pubDate>Tue, 10 Feb 2026 09:00:00 +0200</pubDate><guid>https://man-you.ringum.net/posts/gpu-map-renderer/</guid><description>&lt;p&gt;Building a vector map renderer in Rust with wgpu. From &amp;ldquo;everything renders as points&amp;rdquo; to a full style-driven renderer running on desktop, iOS, and the browser.&lt;/p&gt;
&lt;h2 id="why"&gt;Why&lt;/h2&gt;
&lt;p&gt;We built our maps stack on top of Mapbox GL: the JS SDK, then native SDKs on the C++ core.&lt;/p&gt;
&lt;p&gt;For static maps we wrapped the C++ SDK in a Python library (&lt;a href="https://man-you.ringum.net/backroom/static_maps/"&gt;maparazzo&lt;/a&gt;). It worked but the C++ renderer leaked memory. OpenGL contexts kept state around after objects were destroyed. We tried pooling &lt;code&gt;Map&lt;/code&gt; instances, reusing GL contexts. It helped but never fully solved it.&lt;/p&gt;
&lt;p&gt;I wanted to understand every layer of the stack. What if we built a renderer from scratch, something we fully own, no inherited complexity from a massive C++ codebase?&lt;/p&gt;
&lt;p&gt;Rust + wgpu seemed right. wgpu abstracts over Metal/Vulkan/DX12/WebGPU so you get native + browser from one codebase. Rust gives you memory safety without a GC. No more mystery leaks.&lt;/p&gt;
&lt;h2 id="first-pixels"&gt;First pixels&lt;/h2&gt;
&lt;p&gt;Started from the &lt;a href="https://github.com/nickmass/wgpu-triangle"&gt;wgpu-triangle&lt;/a&gt; example: a bare-bones triangle on screen. Built the stem by hand: tile fetching, MVT decoding, basic projection. From there I heavily leveraged Claude Code to help push through the harder parts and get it to where it is now.&lt;/p&gt;
&lt;p&gt;Everything in one file. &lt;code&gt;main.rs&lt;/code&gt; at 1,500 lines: MVT decoder, protobuf parser, renderer, tile fetching, all of it.&lt;/p&gt;
&lt;p&gt;The MVT decoder is hand-rolled. No prost, no generated code. Protobuf is simple enough at the wire level: varints, length-delimited fields. The tile format uses a local coordinate grid (4096x4096 per tile) with delta-encoded geometry commands.&lt;/p&gt;
&lt;p&gt;First render: every feature drawn as points. Polygons, lines, everything, just dots on screen. But dots in the right places.&lt;/p&gt;
&lt;h2 id="making-it-a-real-map"&gt;Making it a real map&lt;/h2&gt;
&lt;p&gt;Extracted modules one by one. &lt;code&gt;mvt.rs&lt;/code&gt; for tile decoding, &lt;code&gt;renderer.rs&lt;/code&gt; for GPU work, &lt;code&gt;tile_loader.rs&lt;/code&gt; for fetching, &lt;code&gt;tile_coords.rs&lt;/code&gt; for slippy map math.&lt;/p&gt;
&lt;p&gt;Added &lt;code&gt;earcutr&lt;/code&gt; for polygon triangulation: it converts arbitrary polygons with holes into triangles the GPU can draw. First tessellation attempt produced spikes everywhere. The MVT spec requires rings to be closed but some tiles had implicit closure. Two-pass decode: first pass collects keys/values/feature ranges, second pass builds features with proper ring closing.&lt;/p&gt;
&lt;p&gt;Background tile loading with worker threads and &lt;code&gt;mpsc&lt;/code&gt; channels. Scroll zoom, mouse pan. Viewport buffer zone to pre-load surrounding tiles so panning feels instant.&lt;/p&gt;
&lt;p&gt;Tessellation cache keyed by &lt;code&gt;(TileCoord, layer_index, feature_index)&lt;/code&gt;: earcut is expensive, no need to redo it when the camera moves.&lt;/p&gt;
&lt;h2 id="moving-projection-to-the-gpu"&gt;Moving projection to the GPU&lt;/h2&gt;
&lt;p&gt;The big architectural decision. Originally the CPU converted every vertex from lon/lat to screen pixels. Camera move = rebuild all vertices = slow.&lt;/p&gt;
&lt;p&gt;Flipped it: vertices store world lon/lat, the GPU shader does Web Mercator projection. Camera moves only update a 64-byte uniform buffer. Vertex data stays untouched.&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;center_mercator, scale, rotation, viewport_scale, aspect, viewport_size&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;This means &lt;code&gt;mark_camera_dirty()&lt;/code&gt; for pan/zoom (cheap uniform upload), &lt;code&gt;mark_geometry_dirty()&lt;/code&gt; only when tiles arrive or leave. Night and day difference for interactivity.&lt;/p&gt;
&lt;h2 id="style-driven-rendering"&gt;Style-driven rendering&lt;/h2&gt;
&lt;p&gt;A map renderer without style support is just a polygon viewer. Built an expression engine and style evaluator.&lt;/p&gt;
&lt;p&gt;The expression engine handles the Mapbox style spec: &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;has&lt;/code&gt;, &lt;code&gt;match&lt;/code&gt;, &lt;code&gt;case&lt;/code&gt;, &lt;code&gt;interpolate&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt;, &lt;code&gt;let/var&lt;/code&gt;, &lt;code&gt;coalesce&lt;/code&gt;, arithmetic, string ops, comparisons. &lt;code&gt;let/var&lt;/code&gt; was critical: Woosmap styles use variable bindings for i18n name resolution.&lt;/p&gt;
&lt;p&gt;Filter compilation: both legacy &lt;code&gt;[&amp;quot;==&amp;quot;, &amp;quot;key&amp;quot;, val]&lt;/code&gt; and modern &lt;code&gt;[&amp;quot;==&amp;quot;, [&amp;quot;get&amp;quot;,&amp;quot;key&amp;quot;], val]&lt;/code&gt; syntax.&lt;/p&gt;
&lt;p&gt;The key insight for z-ordering: iterate style layers, not MVT layers. The style defines the draw order. For each style layer, find matching features across all tiles.&lt;/p&gt;
&lt;p&gt;Background color comes straight from the style: just set the wgpu clear color.&lt;/p&gt;
&lt;p&gt;Paris at different zoom levels, same style, same renderer, z4 to z16:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/paris_z4_hu_c32f6d7c3bdeea63.webp"
alt="z4"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/paris_z8_hu_cf8793219bec02c5.webp"
alt="z8"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;z4, country level&lt;/td&gt;
&lt;td&gt;z8, region level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/paris_z12_hu_cae9a6fc5531d16f.webp"
alt="z12"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/paris_z16_hu_d8d37d73d56e90d8.webp"
alt="z16"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;z12, city level&lt;/td&gt;
&lt;td&gt;z16, street level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="lines"&gt;Lines&lt;/h2&gt;
&lt;p&gt;Lines in a GPU renderer are not trivial. A &amp;ldquo;line&amp;rdquo; on screen is actually a quad (two triangles) for each segment, with normals perpendicular to the direction. Line joins at vertices need special treatment.&lt;/p&gt;
&lt;p&gt;Implemented miter, bevel, and round joins. Miter joins can spike to infinity at sharp angles, so clamp with a miter limit and fall back to bevel. Round joins emit a fan of triangles.&lt;/p&gt;
&lt;p&gt;Line normals computed in Mercator space, not screen space, because of the GPU projection design. The shader transforms them correctly.&lt;/p&gt;
&lt;h2 id="icons-and-sprites"&gt;Icons and sprites&lt;/h2&gt;
&lt;p&gt;Sprite atlas: load &lt;code&gt;{url}.json&lt;/code&gt; (metadata) + &lt;code&gt;{url}.png&lt;/code&gt; (texture). Each icon is a region in the atlas with UV coordinates.&lt;/p&gt;
&lt;p&gt;SDF (Signed Distance Field) rendering in the fragment shader. One shader handles both SDF sprites and regular RGBA sprites: the &lt;code&gt;is_sdf&lt;/code&gt; field on each vertex switches behavior. SDF gives you clean scaling and halos at any size from a single texture.&lt;/p&gt;
&lt;p&gt;Icon quads: 4 vertices + 6 indices per point. Screen-aligned: the anchor position projects through the map transform but pixel offsets stay fixed.&lt;/p&gt;
&lt;p&gt;@2x sprite support for Retina: load the high-res atlas, halve the pixel sizes.&lt;/p&gt;
&lt;h2 id="text"&gt;Text&lt;/h2&gt;
&lt;p&gt;Text was the biggest single feature. PBF glyph format (same as Mapbox), shelf-packed texture atlas, SDF rendering reusing the icon shader.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GlyphAtlas&lt;/code&gt; dynamically grows (1024x1024 initial, doubles when full). SDF distance stored in alpha channel. The glyph shader is literally the icon shader: &lt;code&gt;is_sdf = 2.0 + halo_buff&lt;/code&gt; triggers PBF glyph mode with the correct SDF thresholds.&lt;/p&gt;
&lt;p&gt;Text shaping is 1:1 codepoint-to-glyph. No GSUB/GPOS, no complex script support yet. Works for Latin, CJK, Cyrillic. Arabic and Thai would need a real shaper like rustybuzz.&lt;/p&gt;
&lt;p&gt;Word wrapping, text-anchor (9 positions), text-offset, text-variable-anchor with collision retry, text-radial-offset, text-justify.&lt;/p&gt;
&lt;h3 id="collision-detection"&gt;Collision detection&lt;/h3&gt;
&lt;p&gt;Without collision detection, labels pile on top of each other. Built a grid-based system.&lt;/p&gt;
&lt;p&gt;Labels grouped by feature: icon + text for the same feature are placed together (all-or-nothing). Groups sorted by &lt;code&gt;(layer_index, symbol-sort-key)&lt;/code&gt;. Grid cells are checked, if occupied the label gets rejected and fades out.&lt;/p&gt;
&lt;p&gt;Variable anchor retry: if a label&amp;rsquo;s primary anchor collides, try alternatives. Pre-compute screen positions for each anchor variant, test them in order. Phase 1: collect decisions. Phase 2: apply vertex shifts. Two phases to avoid borrow conflicts.&lt;/p&gt;
&lt;p&gt;A tour of European cities, all rendered with the same pipeline:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/paris_hu_6211f3a92783251d.webp"
alt="Paris"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/london_hu_25af5b8c4b6a10bc.webp"
alt="London"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/barcelona_hu_b9c4ed6118df85ff.webp"
alt="Barcelona"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paris&lt;/td&gt;
&lt;td&gt;London&lt;/td&gt;
&lt;td&gt;Barcelona&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/amsterdam_hu_7fe112a1b0114b9.webp"
alt="Amsterdam"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/berlin_hu_639b66bd8a5549ec.webp"
alt="Berlin"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/rome_hu_d473484829cc4406.webp"
alt="Rome"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amsterdam&lt;/td&gt;
&lt;td&gt;Berlin&lt;/td&gt;
&lt;td&gt;Rome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/vienna_hu_676fffd2db7f694.webp"
alt="Vienna"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/prague_hu_5ed203892b80f3b3.webp"
alt="Prague"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/budapest_hu_e7f69141d5834c55.webp"
alt="Budapest"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vienna&lt;/td&gt;
&lt;td&gt;Prague&lt;/td&gt;
&lt;td&gt;Budapest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/stockholm_hu_3f396d9181ac3e22.webp"
alt="Stockholm"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/istanbul_hu_86bbb62670f9ce8d.webp"
alt="Istanbul"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/athens_hu_6cfd42c0fad118c3.webp"
alt="Athens"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stockholm&lt;/td&gt;
&lt;td&gt;Istanbul&lt;/td&gt;
&lt;td&gt;Athens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/lisbon_hu_7c15df78ac605bbb.webp"
alt="Lisbon"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/copenhagen_hu_370a9ccc7139df88.webp"
alt="Copenhagen"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/helsinki_hu_f9e1048d2773f1f.webp"
alt="Helsinki"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lisbon&lt;/td&gt;
&lt;td&gt;Copenhagen&lt;/td&gt;
&lt;td&gt;Helsinki&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/oslo_hu_28096293f891d16.webp"
alt="Oslo"
width="512"
height="512"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oslo&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="performance"&gt;Performance&lt;/h2&gt;
&lt;p&gt;Event-driven loop with &lt;code&gt;ControlFlow::Wait&lt;/code&gt;. No busy polling. &lt;code&gt;EventLoopProxy&lt;/code&gt; waker: worker threads signal the event loop when tiles arrive.&lt;/p&gt;
&lt;p&gt;Extract debounce: during active zoom/pan (100ms window), skip full geometry extraction, just update camera uniforms. Once input stops, run the deferred extract. New tile arrivals always trigger extract immediately: you want to see them.&lt;/p&gt;
&lt;p&gt;Zoom style patches: instead of re-extracting everything when zoom changes (colors and sizes are zoom-dependent), patch the extracted vertex data in-place. Rewrites only the color/opacity/width fields.&lt;/p&gt;
&lt;p&gt;Time-sliced extraction: spread heavy work across multiple frames so the renderer doesn&amp;rsquo;t stall.&lt;/p&gt;
&lt;p&gt;Dependencies optimized in debug builds: &lt;code&gt;[profile.dev.package.&amp;quot;*&amp;quot;] opt-level = 2&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="multi-platform"&gt;Multi-platform&lt;/h2&gt;
&lt;h3 id="macos--ios-with-uniffi"&gt;macOS / iOS with UniFFI&lt;/h3&gt;
&lt;p&gt;UniFFI generates Swift bindings from Rust. &lt;code&gt;MapEngine&lt;/code&gt; wraps wgpu in a &lt;code&gt;Mutex&lt;/code&gt;, exposes methods like &lt;code&gt;draw_frame()&lt;/code&gt;, &lt;code&gt;set_center()&lt;/code&gt;, &lt;code&gt;set_zoom()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Metal surface is created from a raw &lt;code&gt;CAMetalLayer&lt;/code&gt; pointer. MSAA disabled in the FFI path because MTKView&amp;rsquo;s MSAA resolve triggers a size mismatch with wgpu. sRGB correction handled by a uniform flag: fragment shaders apply sRGB-to-linear when rendering to MTKView&amp;rsquo;s sRGB surface.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HttpClientDelegate&lt;/code&gt; callback interface: Swift injects a URLSession-backed implementation so network requests go through the platform&amp;rsquo;s native stack.&lt;/p&gt;
&lt;p&gt;xcframework build: &lt;code&gt;aarch64-apple-darwin&lt;/code&gt;, &lt;code&gt;x86_64-apple-darwin&lt;/code&gt;, &lt;code&gt;aarch64-apple-ios&lt;/code&gt;, &lt;code&gt;aarch64-apple-ios-sim&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="browser-with-wasm"&gt;Browser with WASM&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;wasm32-unknown-unknown&lt;/code&gt; target, &lt;code&gt;wasm-bindgen&lt;/code&gt; for the JS interface. Tile loading uses browser &lt;code&gt;fetch()&lt;/code&gt; via &lt;code&gt;web_sys&lt;/code&gt;. No threads in WASM so tile extraction is sequential (10-30 tiles is fine without parallelism).&lt;/p&gt;
&lt;p&gt;Style/sprite loading is async in the WASM path (browser fetch) vs sync on native (ureq). The renderer has platform-agnostic setters: &lt;code&gt;apply_style_data()&lt;/code&gt;, &lt;code&gt;set_sprite_atlas()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;web-time&lt;/code&gt; crate replaces &lt;code&gt;std::time::Instant&lt;/code&gt; everywhere: re-exports std on native, &lt;code&gt;performance.now()&lt;/code&gt; on WASM.&lt;/p&gt;
&lt;h3 id="globe"&gt;Globe&lt;/h3&gt;
&lt;p&gt;Optional 3D sphere projection at low zoom (toggle with G key). The shader branches on a &lt;code&gt;globe_mix&lt;/code&gt; uniform: above 0.5 it projects lon/lat onto a unit sphere, rotated by center latitude and map rotation.&lt;/p&gt;
&lt;p&gt;Back-face discard in the fragment shader: &lt;code&gt;globe_rz &amp;lt; 0.0&lt;/code&gt; hides the back of the sphere. No depth buffer needed.&lt;/p&gt;
&lt;p&gt;Polygon subdivision for the globe: edges longer than 5 degrees get split recursively. Conforming subdivision: all edges of a triangle get split (not just the longest) so adjacent triangles agree on shared edge vertices. No T-junction cracks.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/gpu-map-renderer/globe_france_hu_a8a2967961070efb.webp"
alt="Globe projection centered on France"
width="800"
height="800"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="google-maps-styler"&gt;Google Maps Styler&lt;/h2&gt;
&lt;p&gt;A declarative styling engine that applies Google Maps-style JSON rules to modify layer colors. HSL transforms: hue, saturation, lightness, invert, gamma. Delta-based: rules shift colors rather than setting absolute values.&lt;/p&gt;
&lt;p&gt;Hierarchical feature type matching with a tree structure. &lt;code&gt;&amp;quot;all&amp;quot;&lt;/code&gt; walks the entire tree, &lt;code&gt;&amp;quot;road.highway&amp;quot;&lt;/code&gt; matches just highway layers.&lt;/p&gt;
&lt;p&gt;POI expansion: the styler takes a single POI layer with class metadata and expands enabled classes into individual layers, each with its own filter and icon.&lt;/p&gt;
&lt;h2 id="where-it-stands"&gt;Where it stands&lt;/h2&gt;
&lt;p&gt;This is not done. It&amp;rsquo;s not a replacement for Mapbox GL Native, not even close.&lt;/p&gt;
&lt;p&gt;233 tests. The basics work: fill, line, background, symbol layers with icons and text. Expressions, collision detection, variable anchor, text wrapping, text justification. Runs on macOS, iOS, and the browser. Globe projection at low zoom.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s missing or broken:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Text shaping&lt;/strong&gt; is naive 1:1 codepoint-to-glyph. Arabic, Thai, Burmese, anything that needs ligatures or reordering doesn&amp;rsquo;t work. Need rustybuzz or equivalent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Line rendering&lt;/strong&gt; has no dashes, no gradients, no line-cap styles beyond basic round.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Symbol placement along lines&lt;/strong&gt; is basic. No label curving along the road, no smooth rotation interpolation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collision detection&lt;/strong&gt; is coarse. The grid works but it&amp;rsquo;s not as smart as Mapbox&amp;rsquo;s: no cross-tile collision, no viewport-edge handling for partially visible labels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raster layers&lt;/strong&gt; not supported at all. No hillshade, no satellite imagery compositing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt; is acceptable for interactive use but extraction is still heavier than it should be. Mapbox GL Native has had years of profiling and optimization. We haven&amp;rsquo;t.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fill extrusion&lt;/strong&gt; (3D buildings) not implemented.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotation&lt;/strong&gt; mostly works but some label placement breaks at non-zero bearing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expression coverage&lt;/strong&gt; is incomplete, missing &lt;code&gt;format&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;number-format&lt;/code&gt;, &lt;code&gt;to-color&lt;/code&gt; and several type conversion operators.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;About 15,000 lines of Rust. No C++ dependency. No OpenGL. The foundation is there, the details are not.&lt;/p&gt;</description></item></channel></rss>