<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Alessandro Bahgat&apos;s Blog</title><description>Personal website of Alessandro Bahgat — Software Engineering Leader.</description><link>https://www.abahgat.com</link><item><title>The nearest hospital to every place on Earth, in a single S2 range query</title><link>https://www.abahgat.com/blog/spatial-joins-with-s2</link><guid isPermaLink="true">https://www.abahgat.com/blog/spatial-joins-with-s2</guid><description>For every locality on Earth, how far is the nearest hospital? Naively, that&apos;s 437 billion great-circle distances. S2 cell indexing reduces it to a single integer range-join: build the index once, and the answer for every place on Earth comes back in seconds. The same primitive also resolves which country, region, and locality each hospital belongs to, in a single pass over one index.</description><pubDate>Sun, 31 May 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;How far is the nearest hospital, for every place on Earth?&lt;/em&gt; At first, this
sounds like a distance problem with billions of pairs to check. However,
with the right tools, it isn’t a distance problem at all: with S2 indexing
it’s the same query as &lt;em&gt;which country, region, and locality contains this
point?&lt;/em&gt; We can solve it with a plain integer range check against a single index.&lt;/p&gt;
&lt;p&gt;Last week I was &lt;a href=&quot;https://www.abahgat.com/coaching&quot;&gt;advising a client&lt;/a&gt; whose geo pipeline was
getting slower every week. Their team had started with the obvious approach: given
a few hundred thousand points and several hundred thousand polygons, for
each point they scanned every polygon to find the one that contained it.
As the input size grew, they watched the nightly batch pipeline become
considerably slower and were running out of options.&lt;/p&gt;
&lt;p&gt;As we sketched applicable approaches on a whiteboard, I realized I was
drawing a picture I’d drawn before, a decade before at Google. The
trick, using S2 geometry to turn spatial joins into key joins, is one of
the most elegant and underrated primitives I’ve come across: the kind of
indexing idea that, like &lt;a href=&quot;https://www.abahgat.com/blog/visualizing-ukkonens-algorithm&quot;&gt;Ukkonen’s suffix
trees&lt;/a&gt;, collapses an apparently
quadratic problem into something nearly linear.&lt;/p&gt;
&lt;p&gt;The problem I was solving back then had the same shape but on a different
application: the typical price of a hotel stay anywhere on Earth, any night
of the year, served at 10ms latency, which required precomputing
everything via batch pipelines. One step had to associate every hotel
with every named place that might contain it: neighborhood, town, city,
region, POI.&lt;/p&gt;
&lt;p&gt;Written naively, that step is a cartesian product. Given &lt;code&gt;H&lt;/code&gt; hotels and
&lt;code&gt;R&lt;/code&gt; regions, generate all &lt;code&gt;H x R&lt;/code&gt; pairs and only keep the ones where the
hotel falls inside the region’s polygon.&lt;/p&gt;
&lt;figure class=&quot;asf-root not-prose my-6&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;svg class=&quot;asf-svg&quot; viewBox=&quot;0 0 760 380&quot; role=&quot;img&quot; aria-label=&quot;Left: hotels as dots placed inside three region polygons, each landing in about one region. Right: an H by R matrix comparing every hotel against every region, mostly empty with only a few true containments.&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;!-- ───────────────── Panel A: spatial reality ───────────────── --&gt; &lt;text class=&quot;asf-title&quot; x=&quot;185&quot; y=&quot;32&quot; text-anchor=&quot;middle&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;Spatial reality&lt;/text&gt; &lt;rect class=&quot;asf-card&quot; x=&quot;20&quot; y=&quot;44&quot; width=&quot;330&quot; height=&quot;300&quot; rx=&quot;10&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;rect class=&quot;asf-world&quot; x=&quot;34&quot; y=&quot;64&quot; width=&quot;302&quot; height=&quot;264&quot; rx=&quot;6&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;!-- Region polygons --&gt; &lt;polygon points=&quot;60,90 170,84 188,168 76,182&quot; fill=&quot;#7bbf91&quot; stroke=&quot;#1f5b3a&quot; stroke-width=&quot;1.5&quot; fill-opacity=&quot;0.85&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;212,88 322,98 316,182 218,172&quot; fill=&quot;#f4b95e&quot; stroke=&quot;#a26314&quot; stroke-width=&quot;1.5&quot; fill-opacity=&quot;0.85&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;86,210 252,204 262,300 98,308&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; stroke-width=&quot;1.5&quot; fill-opacity=&quot;0.85&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/polygon&gt; &lt;!-- Region label pills --&gt; &lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;79&quot; y=&quot;90&quot; width=&quot;34&quot; height=&quot;20&quot; rx=&quot;10&quot; fill=&quot;#1f5b3a&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;asf-pill-text&quot; x=&quot;96&quot; y=&quot;100&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; R1 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;239&quot; y=&quot;94&quot; width=&quot;34&quot; height=&quot;20&quot; rx=&quot;10&quot; fill=&quot;#a26314&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;asf-pill-text&quot; x=&quot;256&quot; y=&quot;104&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; R2 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;133&quot; y=&quot;222&quot; width=&quot;34&quot; height=&quot;20&quot; rx=&quot;10&quot; fill=&quot;#2f628f&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;asf-pill-text&quot; x=&quot;150&quot; y=&quot;232&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; R3 &lt;/text&gt; &lt;/g&gt; &lt;!-- Hotel markers + labels --&gt; &lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;118&quot; cy=&quot;138&quot; r=&quot;5.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-hotel-label&quot; x=&quot;128&quot; y=&quot;142&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h1 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;148&quot; cy=&quot;160&quot; r=&quot;5.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-hotel-label&quot; x=&quot;158&quot; y=&quot;164&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h2 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;262&quot; cy=&quot;132&quot; r=&quot;5.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-hotel-label&quot; x=&quot;272&quot; y=&quot;136&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h3 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;300&quot; cy=&quot;256&quot; r=&quot;5.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-hotel-label&quot; x=&quot;286&quot; y=&quot;274&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h4 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;150&quot; cy=&quot;256&quot; r=&quot;5.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-hotel-label&quot; x=&quot;160&quot; y=&quot;260&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h5 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;206&quot; cy=&quot;266&quot; r=&quot;5.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-hotel-label&quot; x=&quot;216&quot; y=&quot;270&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h6 &lt;/text&gt; &lt;/g&gt; &lt;text class=&quot;asf-note&quot; x=&quot;185&quot; y=&quot;364&quot; text-anchor=&quot;middle&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;
Each hotel lands in ~one region, so the answer is sparse.
&lt;/text&gt; &lt;!-- ───────────────── Panel B: naive computation ───────────────── --&gt; &lt;text class=&quot;asf-title&quot; x=&quot;575&quot; y=&quot;32&quot; text-anchor=&quot;middle&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;Naive computation (H × R)&lt;/text&gt; &lt;rect class=&quot;asf-card&quot; x=&quot;410&quot; y=&quot;44&quot; width=&quot;330&quot; height=&quot;300&quot; rx=&quot;10&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;asf-axis&quot; x=&quot;592&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;regions&lt;/text&gt; &lt;text class=&quot;asf-axis&quot; x=&quot;438&quot; y=&quot;209&quot; text-anchor=&quot;middle&quot; transform=&quot;rotate(-90 438 209)&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; hotels &lt;/text&gt; &lt;!-- Region column headers --&gt; &lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;495&quot; y=&quot;72&quot; width=&quot;34&quot; height=&quot;20&quot; rx=&quot;10&quot; fill=&quot;#1f5b3a&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;asf-pill-text&quot; x=&quot;512&quot; y=&quot;82&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; R1 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;575&quot; y=&quot;72&quot; width=&quot;34&quot; height=&quot;20&quot; rx=&quot;10&quot; fill=&quot;#a26314&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;asf-pill-text&quot; x=&quot;592&quot; y=&quot;82&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; R2 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;655&quot; y=&quot;72&quot; width=&quot;34&quot; height=&quot;20&quot; rx=&quot;10&quot; fill=&quot;#2f628f&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;asf-pill-text&quot; x=&quot;672&quot; y=&quot;82&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; R3 &lt;/text&gt; &lt;/g&gt; &lt;!-- Row labels --&gt; &lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;450&quot; cy=&quot;124&quot; r=&quot;4.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-row-label&quot; x=&quot;470&quot; y=&quot;124&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h1 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;450&quot; cy=&quot;158&quot; r=&quot;4.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-row-label&quot; x=&quot;470&quot; y=&quot;158&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h2 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;450&quot; cy=&quot;192&quot; r=&quot;4.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-row-label&quot; x=&quot;470&quot; y=&quot;192&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h3 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;450&quot; cy=&quot;226&quot; r=&quot;4.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-row-label&quot; x=&quot;470&quot; y=&quot;226&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h4 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;450&quot; cy=&quot;260&quot; r=&quot;4.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-row-label&quot; x=&quot;470&quot; y=&quot;260&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h5 &lt;/text&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;circle class=&quot;asf-hotel&quot; cx=&quot;450&quot; cy=&quot;294&quot; r=&quot;4.5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;asf-row-label&quot; x=&quot;470&quot; y=&quot;294&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt; h6 &lt;/text&gt; &lt;/g&gt; &lt;!-- Matrix cells --&gt; &lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;484&quot; y=&quot;111&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; fill=&quot;#7bbf91&quot; stroke=&quot;#1f5b3a&quot; stroke-width=&quot;1.25&quot; fill-opacity=&quot;0.9&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;path class=&quot;asf-check&quot; d=&quot;M 505 124 L 510 129 L 520 118&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/path&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;564&quot; y=&quot;111&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;592&quot; cy=&quot;124&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;644&quot; y=&quot;111&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;672&quot; cy=&quot;124&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;484&quot; y=&quot;145&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; fill=&quot;#7bbf91&quot; stroke=&quot;#1f5b3a&quot; stroke-width=&quot;1.25&quot; fill-opacity=&quot;0.9&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;path class=&quot;asf-check&quot; d=&quot;M 505 158 L 510 163 L 520 152&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/path&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;564&quot; y=&quot;145&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;592&quot; cy=&quot;158&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;644&quot; y=&quot;145&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;672&quot; cy=&quot;158&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;484&quot; y=&quot;179&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;512&quot; cy=&quot;192&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;564&quot; y=&quot;179&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; fill=&quot;#f4b95e&quot; stroke=&quot;#a26314&quot; stroke-width=&quot;1.25&quot; fill-opacity=&quot;0.9&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;path class=&quot;asf-check&quot; d=&quot;M 585 192 L 590 197 L 600 186&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/path&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;644&quot; y=&quot;179&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;672&quot; cy=&quot;192&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;484&quot; y=&quot;213&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;512&quot; cy=&quot;226&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;564&quot; y=&quot;213&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;592&quot; cy=&quot;226&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;644&quot; y=&quot;213&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;672&quot; cy=&quot;226&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;484&quot; y=&quot;247&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;512&quot; cy=&quot;260&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;564&quot; y=&quot;247&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;592&quot; cy=&quot;260&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;644&quot; y=&quot;247&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; stroke-width=&quot;1.25&quot; fill-opacity=&quot;0.9&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;path class=&quot;asf-check&quot; d=&quot;M 665 260 L 670 265 L 680 254&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/path&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;484&quot; y=&quot;281&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;512&quot; cy=&quot;294&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect class=&quot;asf-cell-empty&quot; x=&quot;564&quot; y=&quot;281&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;circle class=&quot;asf-cell-dot&quot; cx=&quot;592&quot; cy=&quot;294&quot; r=&quot;1.8&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/circle&gt; &lt;/g&gt;&lt;g data-astro-cid-qzyn2oct=&quot;&quot;&gt; &lt;rect x=&quot;644&quot; y=&quot;281&quot; width=&quot;56&quot; height=&quot;26&quot; rx=&quot;5&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; stroke-width=&quot;1.25&quot; fill-opacity=&quot;0.9&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/rect&gt; &lt;path class=&quot;asf-check&quot; d=&quot;M 665 294 L 670 299 L 680 288&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;&lt;/path&gt; &lt;/g&gt; &lt;text class=&quot;asf-note&quot; x=&quot;575&quot; y=&quot;364&quot; text-anchor=&quot;middle&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;
But H×R tests every hotel against every region (18 cells, 5 true).
&lt;/text&gt; &lt;/svg&gt; &lt;figcaption class=&quot;asf-caption&quot; data-astro-cid-qzyn2oct=&quot;&quot;&gt;The association step. The answer is a sparse matrix: each hotel falls inside a handful of regions. But the naive cartesian product still runs all H×R point-in-polygon tests to find it.&lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;I’d built it as a Flume pipeline, computing summaries for every night of
the year over data that already lived distributed across several storage backends. Running it on Flume was justified by the layout of the data, the multitude of prices we had, and the 365-day time dimension.
However, joining points-against-polygons was always within
reach of one machine. The primitive, the indexing trick at the heart
of it, never needed a cluster around it.&lt;/p&gt;
&lt;p&gt;To demonstrate it, I wanted to solve a fresh public-data version of
that same problem on a pretty typical machine: an AMD Ryzen 9 7900
desktop (12 cores, 64 GB of RAM). The question I picked: &lt;em&gt;where on Earth is your
nearest hospital?&lt;/em&gt; Its naive form is &lt;strong&gt;437 billion pairs.&lt;/strong&gt; The S2
index collapses it to a single integer range-join: about an hour to
build the index, and then the answer for every locality on Earth comes
back in &lt;strong&gt;seconds&lt;/strong&gt;. This post is the story of how.&lt;/p&gt;
&lt;h2 id=&quot;the-worst-place-to-get-injured&quot;&gt;The worst place to get injured&lt;/h2&gt;
&lt;p&gt;If you live on the Kerguelen archipelago (a French sub-Antarctic
research outpost halfway between Madagascar and Antarctica) and you
need a hospital, the nearest one is &lt;strong&gt;3,362 km away&lt;/strong&gt;, on Rodrigues
Island in Mauritius. That’s farther than New York to Los Angeles. And
it’s the loneliest result in a worldwide leaderboard of localities
ranked by distance to their nearest healthcare facility,
covering every place on Earth that anyone has put on the map.&lt;/p&gt;
&lt;p&gt;The top of the leaderboard is unsurprising: the top three are all settlements on Kerguelen, the next seven are all Tuamotu atolls in French Polynesia.
Interestingly, both are French overseas territories; together they sweep the entire global top 10 because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;each small atoll is its own locality polygon,&lt;/li&gt;
&lt;li&gt;they really are extraordinarily remote, and&lt;/li&gt;
&lt;li&gt;France maps them well.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The last point was interesting: the underlying map data is a treasure, but has varying coverage by country. I suppose it’s due to a combination of factors, one of which being how active the local mapping community is. I guess it makes sense considering how much of it comes from OpenStreetMap volunteers.&lt;/p&gt;
&lt;p&gt;A more interesting exploration asks the question per country, restricted to countries most readers will recognize. Click any row to see the actual location on OpenStreetMap.&lt;/p&gt;































































































&lt;div style=&quot;overflow:auto&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Country&lt;/th&gt;&lt;th&gt;Locality&lt;/th&gt;&lt;th&gt;km&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Russia&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=76.818&amp;amp;mlon=100.780#map=7/76.818/100.780&quot;&gt;Dikson&lt;/a&gt;, Arctic Ocean coast&lt;/td&gt;&lt;td&gt;2,901&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Canada&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=69.208&amp;amp;mlon=-113.831#map=7/69.208/-113.831&quot;&gt;Read Island&lt;/a&gt;, Northwest Territories&lt;/td&gt;&lt;td&gt;2,264&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;United States&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=51.887&amp;amp;mlon=-176.584#map=7/51.887/-176.584&quot;&gt;Adak&lt;/a&gt;, Aleutian Islands, Alaska&lt;/td&gt;&lt;td&gt;1,546&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Greenland &lt;em&gt;(Denmark)&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=60.140&amp;amp;mlon=-45.240#map=7/60.140/-45.240&quot;&gt;Nanortalik&lt;/a&gt;, southern Greenland&lt;/td&gt;&lt;td&gt;1,171&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Australia&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=-31.715&amp;amp;mlon=127.856#map=7/-31.715/127.856&quot;&gt;Mundrabilla&lt;/a&gt;, Nullarbor Plain&lt;/td&gt;&lt;td&gt;631&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;China&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=48.317&amp;amp;mlon=134.654#map=7/48.317/134.654&quot;&gt;黑瞎子岛镇&lt;/a&gt;, Bolshoy Ussuriysky Island&lt;/td&gt;&lt;td&gt;584&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;United Kingdom&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=57.596&amp;amp;mlon=-13.687#map=7/57.596/-13.687&quot;&gt;Rockall&lt;/a&gt;, North Atlantic islet&lt;/td&gt;&lt;td&gt;503&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mauritania&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=16.605&amp;amp;mlon=-8.870#map=7/16.605/-8.870&quot;&gt;Akdernit&lt;/a&gt;, Sahara&lt;/td&gt;&lt;td&gt;365&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Madagascar&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=-19.962&amp;amp;mlon=44.602#map=7/-19.962/44.602&quot;&gt;Beroboka Nord&lt;/a&gt;, western coast&lt;/td&gt;&lt;td&gt;245&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Brazil&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=0.270&amp;amp;mlon=-57.520#map=7/0.270/-57.520&quot;&gt;Oriximiná&lt;/a&gt;, Amazon interior&lt;/td&gt;&lt;td&gt;230&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mali&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=19.945&amp;amp;mlon=2.974#map=7/19.945/2.974&quot;&gt;Tin Zaouatine&lt;/a&gt;, Algerian border / Sahara&lt;/td&gt;&lt;td&gt;219&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Argentina&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=-40.586&amp;amp;mlon=-67.758#map=7/-40.586/-67.758&quot;&gt;Sierra Colorada&lt;/a&gt;, Patagonia&lt;/td&gt;&lt;td&gt;173&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Japan&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=24.465&amp;amp;mlon=131.188#map=7/24.465/131.188&quot;&gt;Kitadaitōjima&lt;/a&gt;, remote Pacific island&lt;/td&gt;&lt;td&gt;165&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Chile&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=-50.713&amp;amp;mlon=-74.341#map=7/-50.713/-74.341&quot;&gt;Natales&lt;/a&gt;, Patagonia / Magallanes&lt;/td&gt;&lt;td&gt;153&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mexico&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=22.420&amp;amp;mlon=-89.715#map=7/22.420/-89.715&quot;&gt;Progreso&lt;/a&gt;, Yucatán&lt;/td&gt;&lt;td&gt;125&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Italy&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=38.706&amp;amp;mlon=13.177#map=8/38.706/13.177&quot;&gt;Ustica&lt;/a&gt;, volcanic island&lt;/td&gt;&lt;td&gt;57&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;France&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.openstreetmap.org/?mlat=48.038&amp;amp;mlon=-4.852#map=8/48.038/-4.852&quot;&gt;Île-de-Sein&lt;/a&gt;, Brittany&lt;/td&gt;&lt;td&gt;45&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;A few things this list reveals (I had to look up every single one of them except for Ustica):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The UK’s most-isolated mapped locality is Rockall&lt;/strong&gt;: a tiny rocky
islet in the North Atlantic, contested between four countries, with
a Royal-Marines-occupied flagpole. So small, it turns out, that nobody lives on it at all (more on that below).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;China’s is Bolshoy Ussuriysky&lt;/strong&gt;: a river island the
PRC and Russia split between them in 2008.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Australia’s Nullarbor Plain&lt;/strong&gt; at 631 km is genuinely remote; the
Outback is really isolated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Italy and France&lt;/strong&gt; show what well-covered countries look like:
the most-isolated locality is a tiny offshore island just
~50 km from the nearest hospital. That’s what most of Europe looks like.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Behind every entry on that table is &lt;strong&gt;one integer per
locality&lt;/strong&gt;, half a million of them, classified against every
healthcare POI in Overture (the open global map dataset, &lt;a href=&quot;#the-data&quot;&gt;introduced
below&lt;/a&gt;), every (locality, POI) pair a
potential great-circle distance. The primitive that makes this possible at scale
is S2 cell indexing, originally built in the mid-2000s to power
Google Maps. The same primitive simultaneously answers a
different-looking question (&lt;em&gt;what country, region, and city does
each hospital belong to?&lt;/em&gt;) from the same index. That’s the part
this post is built around.&lt;/p&gt;
&lt;h2 id=&quot;the-hotels-to-cities-problem-again&quot;&gt;The hotels-to-cities problem, again&lt;/h2&gt;
&lt;p&gt;The Flume pipeline from my memory was a &lt;em&gt;hotels-to-cities&lt;/em&gt; job: for
every (hotel, place) pair, answer &lt;em&gt;is this hotel inside this place?&lt;/em&gt; The
pipeline in my demo works for both &lt;em&gt;hospitals-to-administrative-units&lt;/em&gt; and
&lt;em&gt;hospitals-to-radius-bands&lt;/em&gt;.
&lt;strong&gt;S2 cell indexing&lt;/strong&gt; makes these problems tractable by turning geographic questions (&lt;em&gt;is
point &lt;code&gt;P&lt;/code&gt; inside polygon &lt;code&gt;Q&lt;/code&gt;?&lt;/em&gt;) into an integer-key question (&lt;em&gt;is the
integer &lt;code&gt;P′&lt;/code&gt; in the sorted set &lt;code&gt;Q′&lt;/code&gt;?&lt;/em&gt;). Then, a sort-merge pass handles the rest and the shape of the problem goes from quadratic-on-geometry to
linear-on-integers.&lt;/p&gt;
&lt;p&gt;I wanted to apply the lesson to a fresh problem with public data and
real-world stakes, and settled for these two facets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Distance to the nearest hospital&lt;/em&gt; is something
everybody understands. Mapping every hospital on Earth to every country, region, locality it belongs to, at the same time, is the same question as the
hotels-to-cities use case from my experience.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;How far is the nearest hospital&lt;/em&gt; from each
locality is the same question shape, just with &lt;strong&gt;distance bands
instead of admin levels&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And S2’s hierarchy makes those two kinds of “level” indistinguishable to the algorithm. That’s the surprise from the beginning, spelled out: distance and containment are the same query: let’s see how.&lt;/p&gt;
&lt;h2 id=&quot;s2-in-one-paragraph&quot;&gt;S2 in one paragraph&lt;/h2&gt;
&lt;p&gt;S2 partitions the surface of the Earth into a quadtree of cells. Key principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every cell has a unique 64-bit integer ID;&lt;/li&gt;
&lt;li&gt;every cell has a parent at the next coarser level;&lt;/li&gt;
&lt;li&gt;every leaf cell at level 30 (about 1 cm²) is the descendant of exactly one cell at every level above it;&lt;/li&gt;
&lt;li&gt;it’s possible to walk the S2 hierarchy all the way up to six face cells.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This &lt;em&gt;strict parent-child invariant&lt;/em&gt; — that every
leaf has a unique ancestor at every level — is what makes S2 special.
It lets us encode every cell as an interval &lt;code&gt;[range_min, range_max]&lt;/code&gt;
over leaf-cell IDs. A leaf &lt;code&gt;L&lt;/code&gt; is contained in cell &lt;code&gt;C&lt;/code&gt; if and only if
&lt;code&gt;range_min(C) ≤ L ≤ range_max(C)&lt;/code&gt;. &lt;strong&gt;One integer-interval check&lt;/strong&gt;, no
geometry, and it works at any level, on any cell, against any leaf.
The whole post is built on this one trick.&lt;/p&gt;
&lt;p&gt;Each S2 Cell ID is positional. Three bits pick one of six cube
faces, then two bits per level pick one of four children, and a final
&lt;code&gt;1&lt;/code&gt; bit marks where the cell stops. A coarser cell is therefore a
binary &lt;em&gt;prefix&lt;/em&gt; of all its descendants:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cell C        011 10 00 11 · 1 · 0000…0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;range_min(C)  011 10 00 11 · 0000…00 · 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;range_max(C)  011 10 00 11 · 1111…11 · 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every leaf under &lt;code&gt;C&lt;/code&gt; shares the prefix &lt;code&gt;011 10 00 11&lt;/code&gt; and varies only
in the bits below it, from all-zeros to all-ones. That span is the
interval &lt;code&gt;[range_min, range_max]&lt;/code&gt;, and “is this leaf inside this cell?”
becomes “does this integer fall in that range?”. It’s prefix matching,
the same trick a router uses on a CIDR block. The
Hilbert ordering shown below adds a separate property on top: nearby
cells get nearby IDs, so a whole region covers into just a few of these
intervals instead of thousands.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/hilbert_range.yUtn7Pmn.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1035&quot; height=&quot;1105&quot; srcset=&quot;https://www.abahgat.com/_astro/hilbert_range.yUtn7Pmn_ZrrsLv.webp 400w, https://www.abahgat.com/_astro/hilbert_range.yUtn7Pmn_ZUjhzr.webp 768w, https://www.abahgat.com/_astro/hilbert_range.yUtn7Pmn_2lSUhN.webp 1024w, https://www.abahgat.com/_astro/hilbert_range.yUtn7Pmn_Z22AbEm.webp 1035w, https://www.abahgat.com/_astro/hilbert_range.yUtn7Pmn_1mQtBB.webp 2040w, https://www.abahgat.com/_astro/hilbert_range.yUtn7Pmn_ZnjTFD.webp 2070w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1035px; max-height: 1105px; aspect-ratio: 0.9366515837104072; width: 100%;&quot; alt=&quot;S2 cells on one face at level 4, walked in Hilbert order. The blue space-filling curve is the order S2 uses to number cells: consecutive cells along the curve get consecutive cell IDs. The red square is the 16 leaf-cell descendants of a single parent cell two levels up: they form a contiguous run of cell IDs (a single integer interval) that&apos;s also a connected sub-region of the curve. That&apos;s the property the whole pipeline exploits.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; S2 cells on one face at level 4, walked in Hilbert order. The blue space-filling curve is the order S2 uses to number cells: consecutive cells along the curve get consecutive cell IDs. The red square is the 16 leaf-cell descendants of a single parent cell two levels up: they form a contiguous run of cell IDs (a single integer interval) that&apos;s also a connected sub-region of the curve. That&apos;s the property the whole pipeline exploits. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;h2 id=&quot;the-data&quot;&gt;The data&lt;/h2&gt;
&lt;p&gt;The dataset I used for the demo is &lt;a href=&quot;https://overturemaps.org/&quot;&gt;Overture Maps&lt;/a&gt;,
a public release of Meta’s, Microsoft’s, AWS’s, and TomTom’s joint cleanup and
merge of OpenStreetMap, Microsoft Building Footprints, and assorted
proprietary data. As of the May 2026 release it has 54 million POIs
and 1.07 million administrative polygons, all openly available as
Parquet on S3, byte-range-readable with no auth required.&lt;/p&gt;
&lt;p&gt;I pulled the global tile to my workstation via the
&lt;a href=&quot;https://github.com/OvertureMaps/overturemaps-py&quot;&gt;&lt;code&gt;overturemaps&lt;/code&gt; CLI&lt;/a&gt;
in a few minutes. From that, the input set for this post:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;770,440 healthcare POIs&lt;/strong&gt;: &lt;code&gt;hospital&lt;/code&gt;, &lt;code&gt;medical_center&lt;/code&gt;,
&lt;code&gt;emergency_room&lt;/code&gt;, &lt;code&gt;urgent_care_clinic&lt;/code&gt;. This deliberately excludes
pharmacies, dental clinics, and specialist offices. You could
reproduce the same approach with any other type of POI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;567,307 localities&lt;/strong&gt;: cities, towns, villages, wards.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~4,700 regions&lt;/strong&gt;, &lt;strong&gt;~380 country polygons&lt;/strong&gt; (some countries have
multiple polygons in the source, such as overseas territories,
exclaves, and disputed islands, which get rolled up to one ISO code
at aggregation time).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those three admin levels add up to ~572,000 polygons: a subset of
Overture’s 1.07 million administrative polygons.&lt;/p&gt;
&lt;h2 id=&quot;mapping-every-poi-to-every-admin-level-in-one-pass&quot;&gt;Mapping every POI to every admin level, in one pass&lt;/h2&gt;
&lt;p&gt;For each admin polygon (country, region, locality) we then build its
&lt;strong&gt;cell-union&lt;/strong&gt;: the smallest set of S2 cells whose union covers the
polygon. Each cell carries an &lt;code&gt;INTERIOR&lt;/code&gt; / &lt;code&gt;BOUNDARY&lt;/code&gt; tag. INTERIOR
cells are entirely inside the polygon; BOUNDARY cells straddle its
edge. The construction is a single call to S2’s &lt;code&gt;RegionCoverer&lt;/code&gt;,
wrapped to also tag each cell:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; geo.s2_covering &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cover_polygon&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# `polygon` is a shapely geometry for one admin region.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# `cover_polygon` returns a mixed-level cell-union: small cells along&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# the boundary, big cells inside the bulk.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;tagged &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cover_polygon(polygon, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;min_level&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;max_level&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;12&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;max_cells&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;rows &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    (admin_id, c.range_min, c.range_max, c.tag &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;INTERIOR&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tagged&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Building the cell-union table for those ~572,000 admin polygons produces
&lt;strong&gt;~5.7 million rows&lt;/strong&gt; in 36 minutes on the workstation. Each row is a
small struct: &lt;code&gt;(admin_id, range_min, range_max, is_interior)&lt;/code&gt;.&lt;/p&gt;
&lt;figure class=&quot;cuf-root not-prose my-6&quot; data-cuf-config=&quot;{&amp;quot;dataUrl&amp;quot;:&amp;quot;/data/healthcare-access/figures/italy_cells.json&amp;quot;,&amp;quot;height&amp;quot;:520}&quot;&gt; &lt;div class=&quot;cuf-map relative rounded-lg overflow-hidden border border-border-default&quot; style=&quot;height: 520px;&quot;&gt; &lt;div class=&quot;cuf-canvas absolute inset-0&quot;&gt;&lt;/div&gt; &lt;div class=&quot;cuf-legend absolute bottom-2 left-2 px-3 py-2 rounded text-xs bg-white/90 backdrop-blur border border-border-default text-gray-900 shadow&quot;&gt;&lt;/div&gt; &lt;div class=&quot;cuf-status absolute top-2 right-2 px-2 py-1 text-xs bg-white/85 rounded text-muted&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;figcaption class=&quot;mt-2 text-sm text-muted italic&quot;&gt;Italy&apos;s S2 cell-union at levels 4–7, overlaid on OSM. Green cells (INTERIOR) sit entirely inside the country polygon: a hospital whose leaf cell falls in one of them is auto-confirmed as inside Italy, no further check needed. Orange cells (BOUNDARY) cross the polygon edge; hospitals in them have to be re-checked with a real polygon-contains call. INTERIOR cells are visually rare here, and that&apos;s not a quirk of Italy&apos;s peninsular shape: on every real admin polygon I checked, INTERIOR cells are a small minority of the cell-union. The next section unpacks why the join is still fast.&lt;/figcaption&gt; &lt;/figure&gt; 
&lt;p&gt;For each healthcare POI, I compute its leaf cell, a 64-bit integer.
All 770k of them take four seconds.&lt;/p&gt;
&lt;p&gt;Then a single SQL range-join in DuckDB (conceptually similar to SQLite, but for analytical work on columnar data):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; poi&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;admin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;admin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;subtype&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; poi_leaves poi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;JOIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; unified_cell_union &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;admin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  ON&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; poi&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;leaf&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; BETWEEN&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; admin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;range_min&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; AND&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; admin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;range_max&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the whole spatial join.&lt;/p&gt;
&lt;p&gt;What’s impressive is that it works across all admin levels at once. It
returns ~5.6 million candidate (POI, admin) pairs in under a second:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~965k INTERIOR matches&lt;/strong&gt; are auto-confirmed: leaf is inside an
INTERIOR cell, no further work needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~4.7M BOUNDARY matches&lt;/strong&gt; need a real polygon-contains check; ~3.3M
survive.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The refinement is the only place geometry comes back into the loop. To
keep it cheap, group the candidates by admin polygon: load each
polygon’s geometry once, then call &lt;code&gt;shapely.contains&lt;/code&gt; against every POI that landed in one of its boundary cells:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; shapely.geometry &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Point&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;confirmed &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; admin_id, group &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; boundary_candidates.groupby(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;admin_id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    poly &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; admin_polygons[admin_id]      &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# loaded once per admin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    confirmed.extend(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        (poi_id, admin_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; poi_id, lon, lat &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; group.itertuples(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;index&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; poly.contains(Point(lon, lat))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Polygon loads dominate the runtime; the &lt;code&gt;contains&lt;/code&gt; calls are cheap because the admin polygon was already simplified upstream of the index. ~4.7M candidates → ~3.3M confirmed in a few minutes.&lt;/p&gt;
&lt;p&gt;Total: &lt;strong&gt;~4.2 million confirmed (POI, admin) pairs&lt;/strong&gt;, across country
&lt;em&gt;and&lt;/em&gt; region &lt;em&gt;and&lt;/em&gt; locality, in &lt;strong&gt;6 minutes&lt;/strong&gt; of compute.&lt;/p&gt;
&lt;p&gt;To make the join concrete, take a real hospital: Bergamo’s &lt;em&gt;Ospedale
Papa Giovanni XXIII&lt;/em&gt;, at roughly (45.6917° N, 9.6692° E). Its S2 leaf
cell ID is &lt;code&gt;5,152,488,575,548,925,233&lt;/code&gt;. Each admin polygon is a union
of many cells; the one whose interval contains that integer is its
&lt;em&gt;matching cell&lt;/em&gt;. A cell and its interval are the same object: a cell at
level &lt;em&gt;k&lt;/em&gt; is exactly the leaf range &lt;code&gt;[range_min, range_max]&lt;/code&gt;. The three
matching cells:&lt;/p&gt;





























&lt;div style=&quot;overflow:auto&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Admin polygon&lt;/th&gt;&lt;th&gt;Matching cell&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;range_min&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;range_max&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Italia (country)&lt;/td&gt;&lt;td&gt;level 6&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;5,152,117,973,711,847,425&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;5,152,680,923,665,268,735&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Lombardia (region)&lt;/td&gt;&lt;td&gt;level 7&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;5,152,399,448,688,558,081&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;5,152,540,186,176,913,407&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bergamo (locality)&lt;/td&gt;&lt;td&gt;level 11&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;5,152,488,509,130,407,937&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;5,152,489,058,886,221,823&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Three nested intervals on the same number line, and the hospital’s
leaf cell ID falls inside all three. The SQL above resolves &lt;em&gt;country&lt;/em&gt;,
&lt;em&gt;region&lt;/em&gt;, and &lt;em&gt;locality&lt;/em&gt; containment with one BETWEEN per row,
against one unified table. The usual way to answer “which polygon
contains this point?” is a spatial index like an &lt;a href=&quot;https://en.wikipedia.org/wiki/R-tree&quot;&gt;R-tree&lt;/a&gt;, the structure
behind PostGIS and most geo databases. But an R-tree covers one layer
of polygons at a time, so all three admin levels mean three separate
indexes and three separate tree searches. Three integer comparisons against
one table replace all of it.&lt;/p&gt;
&lt;figure class=&quot;nif-root not-prose my-6&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; &lt;svg class=&quot;nif-svg&quot; viewBox=&quot;0 0 760 340&quot; role=&quot;img&quot; aria-label=&quot;A shared horizontal axis of S2 cell IDs with three rows. Each row is one administrative region&apos;s cell-union, drawn as several scattered cell intervals. In each row one cell is solid: the one containing the hospital&apos;s leaf cell. The three solid cells (a wide blue country cell, a narrower green region cell, and a small amber locality cell) nest inside one another, linked by funnels, and a red dashed vertical line through all three marks the leaf cell.&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; &lt;defs data-astro-cid-dqzbtfef=&quot;&quot;&gt; &lt;marker id=&quot;nif-arrow&quot; markerWidth=&quot;9&quot; markerHeight=&quot;9&quot; refX=&quot;6&quot; refY=&quot;4.5&quot; orient=&quot;auto&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; &lt;path d=&quot;M0,0 L8,4.5 L0,9 Z&quot; class=&quot;nif-arrowhead&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/path&gt; &lt;/marker&gt; &lt;/defs&gt; &lt;text class=&quot;nif-title&quot; x=&quot;380&quot; y=&quot;32&quot; text-anchor=&quot;middle&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;
Each region is a union of cells; one cell per row contains the leaf
&lt;/text&gt; &lt;!-- Containment funnels between consecutive leaf cells --&gt; &lt;polygon class=&quot;nif-funnel&quot; points=&quot;360,114 560,114 545,138 430,138&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/polygon&gt;&lt;polygon class=&quot;nif-funnel&quot; points=&quot;430,168 545,168 525,192 470,192&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/polygon&gt; &lt;!-- Leaf cell dropline: passes through the solid cell of every row --&gt; &lt;line class=&quot;nif-leaf-line&quot; x1=&quot;477&quot; y1=&quot;74&quot; x2=&quot;477&quot; y2=&quot;258&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/line&gt; &lt;!-- Cell-union rows --&gt; &lt;g data-astro-cid-dqzbtfef=&quot;&quot;&gt; &lt;text class=&quot;nif-row-name&quot; x=&quot;172&quot; y=&quot;93&quot; text-anchor=&quot;end&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; Italia &lt;/text&gt; &lt;text class=&quot;nif-row-level&quot; x=&quot;172&quot; y=&quot;108&quot; text-anchor=&quot;end&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; country &lt;/text&gt; &lt;rect class=&quot;nif-cell&quot; x=&quot;210&quot; y=&quot;84&quot; width=&quot;50&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell&quot; x=&quot;268&quot; y=&quot;84&quot; width=&quot;44&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell&quot; x=&quot;320&quot; y=&quot;84&quot; width=&quot;35&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell-leaf&quot; x=&quot;360&quot; y=&quot;84&quot; width=&quot;200&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell&quot; x=&quot;575&quot; y=&quot;84&quot; width=&quot;65&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell&quot; x=&quot;648&quot; y=&quot;84&quot; width=&quot;52&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#8fb8de&quot; stroke=&quot;#2f628f&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt; &lt;/g&gt;&lt;g data-astro-cid-dqzbtfef=&quot;&quot;&gt; &lt;text class=&quot;nif-row-name&quot; x=&quot;172&quot; y=&quot;147&quot; text-anchor=&quot;end&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; Lombardia &lt;/text&gt; &lt;text class=&quot;nif-row-level&quot; x=&quot;172&quot; y=&quot;162&quot; text-anchor=&quot;end&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; region &lt;/text&gt; &lt;rect class=&quot;nif-cell&quot; x=&quot;300&quot; y=&quot;138&quot; width=&quot;38&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#7bbf91&quot; stroke=&quot;#1f5b3a&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell&quot; x=&quot;345&quot; y=&quot;138&quot; width=&quot;75&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#7bbf91&quot; stroke=&quot;#1f5b3a&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell-leaf&quot; x=&quot;430&quot; y=&quot;138&quot; width=&quot;115&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#7bbf91&quot; stroke=&quot;#1f5b3a&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell&quot; x=&quot;560&quot; y=&quot;138&quot; width=&quot;65&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#7bbf91&quot; stroke=&quot;#1f5b3a&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt; &lt;/g&gt;&lt;g data-astro-cid-dqzbtfef=&quot;&quot;&gt; &lt;text class=&quot;nif-row-name&quot; x=&quot;172&quot; y=&quot;201&quot; text-anchor=&quot;end&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; Bergamo &lt;/text&gt; &lt;text class=&quot;nif-row-level&quot; x=&quot;172&quot; y=&quot;216&quot; text-anchor=&quot;end&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt; locality &lt;/text&gt; &lt;rect class=&quot;nif-cell&quot; x=&quot;438&quot; y=&quot;192&quot; width=&quot;30&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#f4b95e&quot; stroke=&quot;#a26314&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell-leaf&quot; x=&quot;470&quot; y=&quot;192&quot; width=&quot;55&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#f4b95e&quot; stroke=&quot;#a26314&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell&quot; x=&quot;528&quot; y=&quot;192&quot; width=&quot;34&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#f4b95e&quot; stroke=&quot;#a26314&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt;&lt;rect class=&quot;nif-cell&quot; x=&quot;566&quot; y=&quot;192&quot; width=&quot;29&quot; height=&quot;30&quot; rx=&quot;4&quot; fill=&quot;#f4b95e&quot; stroke=&quot;#a26314&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/rect&gt; &lt;/g&gt; &lt;!-- Shared number line --&gt; &lt;line class=&quot;nif-axis&quot; x1=&quot;180&quot; y1=&quot;258&quot; x2=&quot;716&quot; y2=&quot;258&quot; marker-end=&quot;url(#nif-arrow)&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/line&gt; &lt;text class=&quot;nif-axis-label&quot; x=&quot;180&quot; y=&quot;282&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;S2 cell ID: one 64-bit integer axis&lt;/text&gt; &lt;!-- Leaf marker on the axis + label --&gt; &lt;circle class=&quot;nif-leaf-dot&quot; cx=&quot;477&quot; cy=&quot;258&quot; r=&quot;4.5&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;nif-leaf-label&quot; x=&quot;477&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;hospital leaf cell&lt;/text&gt; &lt;text class=&quot;nif-leaf-value&quot; x=&quot;477&quot; y=&quot;282&quot; text-anchor=&quot;middle&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;5,152,488,575,548,925,233&lt;/text&gt; &lt;text class=&quot;nif-note&quot; x=&quot;380&quot; y=&quot;326&quot; text-anchor=&quot;middle&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;
Schematic, not to scale. Bergamo&apos;s real cells are ~0.1% the size of Italy&apos;s, and a country&apos;s union runs to ~200
      cells.
&lt;/text&gt; &lt;/svg&gt; &lt;figcaption class=&quot;nif-caption&quot; data-astro-cid-dqzbtfef=&quot;&quot;&gt;Each administrative level&apos;s cell-union is a set of S2 cells (the table above shows just the one cell per level that matches here). Drawn on a single axis of cell IDs, the solid cell in each row is the one whose interval contains the hospital&apos;s leaf cell (red); those three are nested, so one BETWEEN per row resolves country, region, and locality containment together.&lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;The cell-union skews heavily toward BOUNDARY cells. Real admin polygons
have long, jagged perimeters relative to their area, and &lt;code&gt;RegionCoverer&lt;/code&gt;
adaptively subdivides only the edge cells: each level of refinement
turns ~1 straddling parent into ~2 straddling children, so a few edge
cells at coarse levels balloon into many leaf BOUNDARY cells at
&lt;code&gt;max_level&lt;/code&gt;. Meanwhile the polygon’s bulk gets covered by a handful of
large INTERIOR cells.&lt;/p&gt;
&lt;p&gt;That skew doesn’t slow the join down, though. The range-join itself is
just &lt;code&gt;BETWEEN&lt;/code&gt; comparisons, so it doesn’t care how the cells are tagged.
The only real cost is the BOUNDARY refinement, and most of those checks
land on leaf cells at &lt;code&gt;max_level&lt;/code&gt;, where the geometries are small and
&lt;code&gt;shapely.contains&lt;/code&gt; runs in microseconds. INTERIOR cells help at the
margin, auto-confirming the POIs that fall in a polygon’s bulk, but the
join is fast mainly because even the boundary path is cheap: a tiny
leaf, an already-simplified polygon, a microsecond contains call.&lt;/p&gt;
&lt;p&gt;The refinement work scales with polygon &lt;strong&gt;perimeter&lt;/strong&gt; at max_level, not
with area.&lt;/p&gt;
&lt;h2 id=&quot;distance-is-just-another-kind-of-hierarchy&quot;&gt;Distance is just another kind of hierarchy&lt;/h2&gt;
&lt;p&gt;Now the harder-looking question: &lt;em&gt;how far is the nearest hospital from
each locality?&lt;/em&gt; The naive solution is to treat it as a nearest-neighbor problem: for each
locality, scan all hospitals, find the closest. Even with a spatial
index it’s a different kind of problem from the admin-rollup join.&lt;/p&gt;
&lt;p&gt;Is it though?&lt;/p&gt;
&lt;p&gt;For each healthcare POI, we can build a spherical cap at each radius band
(&lt;em&gt;within 1 km&lt;/em&gt;, &lt;em&gt;within 5 km&lt;/em&gt;, &lt;em&gt;within 15 km&lt;/em&gt;, &lt;em&gt;within 30 km&lt;/em&gt;, &lt;em&gt;within
100 km&lt;/em&gt;) and cover each cap with S2 cells. The construction in S2 is
about as short as it gets:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; s2sphere&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;EARTH_RADIUS_KM&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6371.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;center &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; s2sphere.LatLng.from_degrees(poi.lat, poi.lon).to_point()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;cap &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; s2sphere.Cap.from_axis_angle(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    center,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    s2sphere.Angle.from_radians(radius_km &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; EARTH_RADIUS_KM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;cells &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; region_coverer.get_covering(cap)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get_covering&lt;/code&gt; is the same primitive used for the admin polygons;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cap&lt;/code&gt; is just a shape &lt;code&gt;RegionCoverer&lt;/code&gt; knows how to cover.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;scf-root not-prose my-6&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt; &lt;svg class=&quot;scf-svg&quot; viewBox=&quot;0 0 780 430&quot; role=&quot;img&quot; aria-label=&quot;Left: a sphere representing the Earth with radius R. An axis runs from the centre to a hospital on the surface. The region of the surface within an angle theta of that axis is shaded as a cap, bounded by a small circle drawn in perspective. The half-angle at the centre is labelled theta equals r over R. Right: the flat-plane analog, where the same within-r region is a flat disk around the hospital. Together they show that on a curved surface the disk wraps into a spherical cap.&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt; &lt;text class=&quot;scf-title&quot; x=&quot;390&quot; y=&quot;30&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt; “Within r km of a hospital” is a spherical cap &lt;/text&gt; &lt;!-- ===== Left: sphere + cap ===== --&gt; &lt;!-- sphere body --&gt; &lt;circle class=&quot;scf-sphere&quot; cx=&quot;250&quot; cy=&quot;232&quot; r=&quot;140&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/circle&gt; &lt;!-- equator for globe feel: back half dashed, front half solid --&gt; &lt;path class=&quot;scf-equator-back&quot; d=&quot;M 110 232 A 140 42 0 0 0 390 232&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/path&gt; &lt;path class=&quot;scf-equator-front&quot; d=&quot;M 110 232 A 140 42 0 0 1 390 232&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/path&gt; &lt;!-- cap dome --&gt; &lt;path class=&quot;scf-cap&quot; d=&quot;M 160.0 124.8 A 140 140 0 0 1 340.0 124.8 A 90.0 17 0 0 1 160.0 124.8 Z&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/path&gt; &lt;!-- cap rim small-circle --&gt; &lt;path class=&quot;scf-rim-back&quot; d=&quot;M 160.0 124.8 A 90.0 17 0 0 0 340.0 124.8&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/path&gt; &lt;path class=&quot;scf-rim-front&quot; d=&quot;M 160.0 124.8 A 90.0 17 0 0 1 340.0 124.8&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/path&gt; &lt;!-- radii to the cap edges + axis --&gt; &lt;line class=&quot;scf-radius&quot; x1=&quot;250&quot; y1=&quot;232&quot; x2=&quot;160.00973464388449&quot; y2=&quot;124.75377796334308&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/line&gt; &lt;line class=&quot;scf-radius&quot; x1=&quot;250&quot; y1=&quot;232&quot; x2=&quot;339.9902653561155&quot; y2=&quot;124.75377796334308&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/line&gt; &lt;line class=&quot;scf-axis&quot; x1=&quot;250&quot; y1=&quot;232&quot; x2=&quot;250&quot; y2=&quot;74&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/line&gt; &lt;!-- half-angle wedge --&gt; &lt;path class=&quot;scf-angle&quot; d=&quot;M 250 180 A 52 52 0 0 0 216.6 192.2&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/path&gt; &lt;text class=&quot;scf-eq&quot; x=&quot;308&quot; y=&quot;204&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;θ = r / R&lt;/text&gt; &lt;!-- highlighted surface distance r --&gt; &lt;path class=&quot;scf-surface&quot; d=&quot;M 250 92 A 140 140 0 0 0 160.0 124.8&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/path&gt; &lt;text class=&quot;scf-r-label&quot; x=&quot;188.11717993440638&quot; y=&quot;94.44303308997283&quot; text-anchor=&quot;end&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;r&lt;/text&gt; &lt;!-- centre + hospital --&gt; &lt;circle class=&quot;scf-centre&quot; cx=&quot;250&quot; cy=&quot;232&quot; r=&quot;3&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;scf-muted&quot; x=&quot;258&quot; y=&quot;248&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;centre, radius R&lt;/text&gt; &lt;circle class=&quot;scf-poi&quot; cx=&quot;250&quot; cy=&quot;92&quot; r=&quot;6&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;scf-poi-label&quot; x=&quot;250&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;hospital&lt;/text&gt; &lt;text class=&quot;scf-muted&quot; x=&quot;250&quot; y=&quot;80&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;(cap axis)&lt;/text&gt; &lt;!-- cap callout --&gt; &lt;text class=&quot;scf-cap-label&quot; x=&quot;250&quot; y=&quot;116.75377796334308&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;spherical cap&lt;/text&gt; &lt;!-- ===== Right: flat-plane disk ===== --&gt; &lt;text class=&quot;scf-panel-title&quot; x=&quot;592&quot; y=&quot;92&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;flat-plane analog&lt;/text&gt; &lt;polygon class=&quot;scf-plane&quot; points=&quot;478,250 752,250 706,330 432,330&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/polygon&gt; &lt;ellipse class=&quot;scf-disk&quot; cx=&quot;592&quot; cy=&quot;290&quot; rx=&quot;74&quot; ry=&quot;23&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/ellipse&gt; &lt;line class=&quot;scf-disk-r&quot; x1=&quot;592&quot; y1=&quot;290&quot; x2=&quot;666&quot; y2=&quot;290&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/line&gt; &lt;text class=&quot;scf-r-label&quot; x=&quot;629&quot; y=&quot;282&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;r&lt;/text&gt; &lt;circle class=&quot;scf-poi&quot; cx=&quot;592&quot; cy=&quot;290&quot; r=&quot;5.5&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;&lt;/circle&gt; &lt;text class=&quot;scf-disk-cap&quot; x=&quot;592&quot; y=&quot;128&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;a disk&lt;/text&gt; &lt;!-- bridge note between panels --&gt; &lt;text class=&quot;scf-bridge&quot; x=&quot;592&quot; y=&quot;158&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;curve the plane →&lt;/text&gt; &lt;text class=&quot;scf-bridge&quot; x=&quot;592&quot; y=&quot;174&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;the disk becomes a cap&lt;/text&gt; &lt;text class=&quot;scf-note&quot; x=&quot;390&quot; y=&quot;416&quot; text-anchor=&quot;middle&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;
Schematic, not to scale: θ is drawn far wider than any real band — 100 km is ≈ 0.9° on Earth. The code passes θ =
      radius_km / EARTH_RADIUS_KM, the arc radius in radians.
&lt;/text&gt; &lt;/svg&gt; &lt;figcaption class=&quot;scf-caption&quot; data-astro-cid-45pcwwif=&quot;&quot;&gt;Why a &lt;em&gt;cap&lt;/em&gt;, not a circle: the Earth is a sphere, so “within r km of a hospital” is the patch of surface within an angular radius θ of the axis pointing at that hospital: a spherical cap. The angle is all S2 needs: θ = radius_km / EARTH_RADIUS_KM, exactly the value passed to &lt;code&gt;Cap.from_axis_angle&lt;/code&gt;. On a flat plane the same region would be a disk; wrapping the plane onto the sphere turns it into the cap, which keeps the bands honest out to the 100 km radius.&lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;Then we take the per-radius union across all 770k POIs. The result is &lt;strong&gt;five cell-unions&lt;/strong&gt; (one per radius band), each
tagging the part of the planet that’s within that radius of &lt;em&gt;some&lt;/em&gt;
hospital.&lt;/p&gt;
&lt;p&gt;Then, for each locality, we take its representative point’s leaf cell and ask:
&lt;em&gt;what’s the smallest band whose cell-union contains me?&lt;/em&gt; That’s its
isolation distance.&lt;/p&gt;
&lt;p&gt;The check is identical to the admin one: &lt;code&gt;leaf BETWEEN range_min AND range_max&lt;/code&gt;. The hierarchy now spans distance scales instead of administrative
scales, and the algorithm doesn’t know or care about the difference.
I see that as more proof of the elegance in the S2 design: country / region / locality / 1 km / 5 km / 30 km
are all the same kind of thing to the index. They are all representable as nested cell-unions over
a quadtree, and queryable with a single integer-interval check.&lt;/p&gt;
&lt;figure class=&quot;rbf-root not-prose my-6&quot; data-rbf-config=&quot;{&amp;quot;dataUrl&amp;quot;:&amp;quot;/data/healthcare-access/figures/reykjavik_bands.json&amp;quot;,&amp;quot;height&amp;quot;:520}&quot;&gt; &lt;div class=&quot;rbf-map relative rounded-lg overflow-hidden border border-border-default&quot; style=&quot;height: 520px;&quot;&gt; &lt;div class=&quot;rbf-canvas absolute inset-0&quot;&gt;&lt;/div&gt; &lt;div class=&quot;rbf-legend absolute bottom-2 left-2 px-3 py-2 rounded text-xs bg-white/90 backdrop-blur border border-border-default text-gray-900 shadow&quot;&gt;&lt;/div&gt; &lt;div class=&quot;rbf-status absolute top-2 right-2 px-2 py-1 text-xs bg-white/85 rounded text-muted&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;figcaption class=&quot;mt-2 text-sm text-muted italic&quot;&gt;Five concentric radius-band S2 cell-unions around one example hospital in Reykjavik, overlaid on OSM. Each colored band is everything within 1 / 5 / 15 / 30 / 100 km of the hospital, covered with S2 cells whose [range_min, range_max] intervals can be checked against a leaf cell in a single integer comparison. Bigger radii merge into coarser parent cells, the same Hilbert-locality property that made the country figure work. To the algorithm, this picture and Italy&apos;s are the same class of thing.&lt;/figcaption&gt; &lt;/figure&gt; 
&lt;p&gt;This is where S2 separates from &lt;a href=&quot;https://h3geo.org/&quot;&gt;H3&lt;/a&gt;, Uber’s
hexagonal grid system, and from R-trees. R-trees can do
range queries but need one tree per question: four levels of
hierarchy = four trees, four traversals. H3 can index polygons but its
parent-child relation is &lt;em&gt;approximate&lt;/em&gt;, so a multi-level union breaks
the integer-interval trick. S2’s strict quadtree parent-child invariant
is the property that makes the same primitive work for both kinds of
hierarchy.&lt;/p&gt;
&lt;p&gt;Here are the cell counts per band, after normalization:&lt;/p&gt;





























&lt;div style=&quot;overflow:auto&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Radius&lt;/th&gt;&lt;th&gt;Cells (after normalize)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;1 km&lt;/td&gt;&lt;td&gt;1,575,449&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;5 km&lt;/td&gt;&lt;td&gt;724,157&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;15 km&lt;/td&gt;&lt;td&gt;317,056&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;30 km&lt;/td&gt;&lt;td&gt;129,497&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;100 km&lt;/td&gt;&lt;td&gt;11,021&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Bigger radii merge into coarser parents, yielding fewer cells for the same fidelity.
Building all five bands across 770k POIs took 30 minutes; classifying
567k localities into bands took 30 seconds.&lt;/p&gt;
&lt;h2 id=&quot;a-booby-trap&quot;&gt;A booby-trap&lt;/h2&gt;
&lt;p&gt;The first version of the global leaderboard had &lt;strong&gt;Archipel des Crozet
at 2,008 km&lt;/strong&gt;. Being unfamiliar with many of these locations, I decided to do some spot-checking.
Crozet is a sub-Antarctic French island group in the southern Indian Ocean, about as far from anything as land gets, so I
expected a big number. But 2,008 felt &lt;em&gt;too low&lt;/em&gt;: Crozet to Cape Town
is 2,400 km, Crozet to Madagascar is 2,400 km, and there’s nothing in
between. A hospital can’t sit closer than the nearest land, I thought. So where was this 2,008-km hospital?&lt;/p&gt;
&lt;p&gt;A query of my data gave me the answer: at coordinates &lt;strong&gt;(-30.75°, 63.63°)&lt;/strong&gt;, the middle of the open Indian
Ocean. Querying that location in Overture’s data turned up:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;서울병원 (Seoul Hospital), country=KR&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A Korean hospital tagged ~10,000 km from Korea, due to a geocoding error
somewhere upstream in the OSM ingest. That phantom POI was the
“nearest hospital” pulling Crozet down to 2,008 km. It did worse
damage next door: it also sat ~2,150 km from Kerguelen, well inside
Kerguelen’s true ~3,360 km, so it had also quietly bumped the genuine top
results off the board. Three more ghosts turned up at similar mid-ocean
coordinates, each understating how isolated some real place is.&lt;/p&gt;
&lt;p&gt;The fix is one filter, against the same table we built for the join:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- Keep POIs whose leaf falls inside some country polygon.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- Misgeocoded ocean POIs never match because no country range covers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- the open ocean.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT DISTINCT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; poi&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; poi_leaves poi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;JOIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; unified_cell_union &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;admin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  ON&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; poi&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;leaf&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; BETWEEN&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; admin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;range_min&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; AND&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; admin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;range_max&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt; AND&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; admin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;subtype&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;country&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the same &lt;code&gt;BETWEEN&lt;/code&gt; primitive, restricted to one admin level.
The index doesn’t need to know what a “country” is; it just trusts
the cell-union for &lt;code&gt;subtype = &apos;country&apos;&lt;/code&gt;. After the filter, Kerguelen
reclaims the top three and Crozet settles at its real ~2,400 km
(nearest hospital in Madagascar): still extraordinarily remote, just
below the Kerguelen and Tuamotu tier at the top of this post.&lt;/p&gt;
&lt;p&gt;The lesson, more useful than the leaderboard: &lt;strong&gt;even with a clean
algorithm and public data on a workstation, the first answer is wrong
in interesting ways.&lt;/strong&gt; The question shape is right; the data has
booby-traps; you find them by looking at outliers and asking “does
this make sense geographically?”&lt;/p&gt;
&lt;p&gt;You’ll often hear advice that in any data problem (whether it’s for ML or data analysis), spending time eyeballing the data for patterns and outliers pays off. This is one more example of that.&lt;/p&gt;
&lt;h2 id=&quot;see-it&quot;&gt;See it&lt;/h2&gt;
&lt;div class=&quot;ihav-root not-prose rounded-lg border border-border-default overflow-hidden&quot; data-ihav-height=&quot;700px&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt; &lt;div class=&quot;ihav-controls flex flex-wrap items-center gap-3 p-3 border-b border-border-default bg-card text-sm&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt; &lt;!-- Mode segmented control: Isolation vs Density --&gt; &lt;div class=&quot;flex items-center gap-2&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt; &lt;span class=&quot;text-xs font-medium text-muted&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;Mode&lt;/span&gt; &lt;div class=&quot;ihav-mode-group inline-flex rounded-md border border-border-default overflow-hidden text-default&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt; &lt;button data-mode=&quot;isolation&quot; class=&quot;ihav-mode ihav-mode-active px-3 py-1.5 text-xs font-medium transition-colors&quot; aria-pressed=&quot;true&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;
Isolation
&lt;/button&gt; &lt;button data-mode=&quot;density&quot; class=&quot;ihav-mode px-3 py-1.5 text-xs font-medium border-l border-border-default transition-colors&quot; aria-pressed=&quot;false&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;
Density
&lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Show S2 cells overlay toggle (independent from Mode) --&gt; &lt;button type=&quot;button&quot; class=&quot;ihav-cells-toggle inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-border-default text-default transition-colors&quot; aria-pressed=&quot;false&quot; title=&quot;Overlay the S2 cell-union used by the join&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt; &lt;svg width=&quot;13&quot; height=&quot;13&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt; &lt;rect x=&quot;3&quot; y=&quot;3&quot; width=&quot;7&quot; height=&quot;7&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;&lt;/rect&gt; &lt;rect x=&quot;14&quot; y=&quot;3&quot; width=&quot;7&quot; height=&quot;7&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;&lt;/rect&gt; &lt;rect x=&quot;3&quot; y=&quot;14&quot; width=&quot;7&quot; height=&quot;7&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;&lt;/rect&gt; &lt;rect x=&quot;14&quot; y=&quot;14&quot; width=&quot;7&quot; height=&quot;7&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;&lt;/rect&gt; &lt;/svg&gt; &lt;span data-astro-cid-mz4n33uv=&quot;&quot;&gt;Show S2 cells&lt;/span&gt; &lt;/button&gt; &lt;span class=&quot;ihav-status text-xs text-muted ml-auto&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;&lt;/span&gt; &lt;/div&gt; &lt;div class=&quot;ihav-viz relative w-full&quot; style=&quot;height: 700px; min-height: 420px; background: #f4ecd8;&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt; &lt;div class=&quot;ihav-map absolute inset-0&quot; style=&quot;width: 100%; height: 100%;&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;&lt;/div&gt; &lt;div class=&quot;ihav-tooltip absolute pointer-events-none px-3 py-2 rounded text-xs leading-tight bg-card border border-border-default shadow-md text-default opacity-0 transition-opacity z-10&quot; style=&quot;max-width: 280px;&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;&lt;/div&gt; &lt;div class=&quot;ihav-legend absolute bottom-3 left-3 px-3 py-2 rounded-md text-xs leading-snug shadow-md bg-white dark:bg-page border border-border-default text-default&quot; style=&quot;max-width: min(28rem, calc(100% - 1.5rem));&quot; data-astro-cid-mz4n33uv=&quot;&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; 
&lt;p&gt;The interactive map above zooms through three layers of the same join focused on my home country: Italy.
20 regioni, 8,577 comuni, and 12,579 healthcare POIs. Polygon fills are
colored by &lt;em&gt;what fraction of contained comuni are within 5 km of
healthcare&lt;/em&gt;: blue good, red bad, cream in the middle. Hover any
feature for its per-area stats. Toggle “Density” to switch from access
to healthcare-POIs-per-km².&lt;/p&gt;
&lt;p&gt;Why Italy specifically? Of course I have ties to the country, but beyond that, there are more interesting reasons.&lt;/p&gt;
&lt;p&gt;While the algorithm is universal, the
&lt;em&gt;input data&lt;/em&gt; isn’t: Overture’s locality coverage is dense across Italy
(every comune mapped) and several other countries, but patchy elsewhere. Showing the join over Italy
keeps the demo interesting and realistic: every visible polygon is a real comune with a
real population, not a hole in OSM’s coverage. When I worked on this problem at Google, we were also dependent on the quality of data from Google Maps, and we knew that some countries were better mapped than others.&lt;/p&gt;
&lt;p&gt;A few patterns the Italian view makes obvious:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;alpine arc&lt;/strong&gt; (Valle d’Aosta, Trentino, the northern fringe of
Lombardia and Veneto) reads warmer: small mountain comuni without
their own hospital, leaning on the valley town next door. An interesting follow-up study might be considering &lt;em&gt;driving distance&lt;/em&gt;, which I’d expect to affect POI accessibility even more.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Po Valley, Tuscany, Campania around Naples, the Adriatic coast&lt;/strong&gt;
read cool: dense comune mosaic, dense hospital coverage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sardinia and Sicily&lt;/strong&gt; show internal isolation: interior mountain
comuni warm, coastal comuni cool.&lt;/li&gt;
&lt;li&gt;Zoom in far enough and the &lt;strong&gt;POI dots&lt;/strong&gt; light up: deep red for
hospitals, blue for emergency rooms.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;see-the-join-structure-itself&quot;&gt;See the join structure itself&lt;/h3&gt;
&lt;p&gt;Flip on &lt;strong&gt;Show S2 cells&lt;/strong&gt; in the toolbar. The choropleth gets overlaid
with the actual S2 cell-union used by the join: green cells are
INTERIOR (entirely inside the polygon, auto-confirmed in the range
query), orange cells are BOUNDARY (straddle the edge, refined via
&lt;code&gt;shapely.contains&lt;/code&gt;). At low zoom you see the per-regione cell-union; at
z7–z8 it hands off to the per-comune cell-unions, the same parent-child
hand-off the algorithm relies on. Around that zoom level you can briefly
see &lt;em&gt;both&lt;/em&gt; layers on top of each other: a Lombardia-tier cell sitting
above the Milan / Monza / Lodi comune cells it contains. That visual
nesting &lt;em&gt;is&lt;/em&gt; the integer-interval check from the SQL block earlier in
the post.&lt;/p&gt;
&lt;p&gt;One thing the overlay makes visible: &lt;strong&gt;the INTERIOR/BOUNDARY split
looks completely different depending on what you count.&lt;/strong&gt; Barely
&lt;strong&gt;1% of the cells in Italy’s cell-union are INTERIOR&lt;/strong&gt;; nearly all the
rest trace the jagged comune and regione edges. That sounds like the
algorithm’s “fast path” is doing almost no work, but the &lt;em&gt;matches&lt;/em&gt; skew
far more INTERIOR than the cells do, because POIs cluster in the bulk of
polygons, not on their edges, exactly as the join section above
described.&lt;/p&gt;
&lt;h2 id=&quot;what-the-answer-doesnt-and-cant-say&quot;&gt;What the answer doesn’t (and can’t) say&lt;/h2&gt;
&lt;p&gt;Four caveats deserve more than a footnote:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OSM coverage varies by country.&lt;/strong&gt; The dataset’s tagging fidelity
isn’t uniform. Italy’s espresso bars are tagged &lt;code&gt;bar&lt;/code&gt; and not
&lt;code&gt;coffee_shop&lt;/code&gt;; Vietnam tags every cà phê; Thailand tags every clinic;
rural Russia barely tags anything. For &lt;em&gt;isolation distance&lt;/em&gt;, this means
the answer is an &lt;strong&gt;upper bound&lt;/strong&gt; wherever coverage is thin: when we
say “120 km to the nearest hospital,” the truth is “120 km to the
nearest hospital that someone bothered to map.” That’s the right
answer to &lt;em&gt;what does the public dataset say&lt;/em&gt;. It’s not the answer to
&lt;em&gt;where are the actual hospital deserts&lt;/em&gt; without continued investment in
mapping ground truth as open data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Density is partly a tagging-density story.&lt;/strong&gt; The “densest healthcare”
leaderboard is dominated by Bangkok wards (top entry: 254 healthcare
POIs per km², four of the top five are in central Bangkok), then Delhi,
Jakarta, Taipei, Seoul, Saigon. Bangkok’s medical-tourism culture and
high small-clinic density are real. But Thai OSM mappers are also
unusually thorough. The dataset’s leaderboards are always partly a
leaderboard of &lt;em&gt;map quality&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Outright vandalism happens.&lt;/strong&gt; OSM is open; some entries are jokes.
Greenland’s top-isolated locality in the raw data came back as a
Skibidi-meme joke name that someone added to the map. Same problem as
the misgeocoded ocean POIs: once you look at the answers, you find
them. (Filter that one and the next entry, a real Inuit settlement on
the east coast, slides up.) The shape of the problem is the same: a
public dataset’s leaderboard is downstream of the public dataset’s
quality, and you treat it as data not as truth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A locality isn’t necessarily inhabited.&lt;/strong&gt; When I looked into the
UK’s lonely winner, Rockall, it turned out to have no permanent
population at all. Nothing is wrong with the data, though: Overture
&lt;em&gt;has&lt;/em&gt; an optional &lt;code&gt;population&lt;/code&gt; field, but it’s sparse, and the
&lt;code&gt;locality&lt;/code&gt; subtype only asserts that someone put a &lt;em&gt;named place&lt;/em&gt; on
the map, not that anyone lives there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Locality coverage is wildly uneven across countries, which is why
the live demo in this post is Italy.&lt;/strong&gt; Overture inherits OSM’s
country-level mapping density: Italy has &lt;strong&gt;8,577 mapped comuni&lt;/strong&gt;,
France 36,752 communes, Germany 22,967. Greece has &lt;strong&gt;178&lt;/strong&gt;. Within the
US, Massachusetts is fully tiled with mapped places; Virginia is full
of gaps. A global choropleth at city zoom reads great over Western
Europe and becomes visibly patchy elsewhere, not because the algorithm
breaks, but because the &lt;em&gt;input data&lt;/em&gt; is uneven. The interactive in
this post uses Italy because every visible polygon is a real comune
with a real population.&lt;/p&gt;
&lt;h2 id=&quot;why-this-is-an-s2-post-and-not-just-a-use-a-spatial-index-post&quot;&gt;Why this is an S2 post and not just a “use a spatial index” post&lt;/h2&gt;
&lt;p&gt;The cell-set inner join, on its own, doesn’t distinguish S2 from any
other spatial index. R-trees do range queries. H3 cells can index
polygons. DuckDB’s plain &lt;code&gt;ST_Contains&lt;/code&gt; is already quite fast at this
scale on its own: for a single-level point-in-polygon problem, a
generic spatial join would land in the same ballpark.&lt;/p&gt;
&lt;p&gt;What S2 buys with its versatility, and what this post is built to show, is &lt;strong&gt;one index, every scale at once&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The same cell-union table resolves country &lt;em&gt;and&lt;/em&gt; region &lt;em&gt;and&lt;/em&gt;
locality for any point: single integer-interval lookup against a
unified table.&lt;/li&gt;
&lt;li&gt;The same primitive resolves &lt;em&gt;what 1 km / 5 km / 30 km radius am I in&lt;/em&gt;
for any point: same lookup, separately-built but structurally
identical table.&lt;/li&gt;
&lt;li&gt;The two kinds of “level” are the &lt;em&gt;same kind of thing&lt;/em&gt; to the
algorithm. R-trees would need four trees and four traversals. H3
&lt;em&gt;has&lt;/em&gt; parents; that’s not the issue. The issue is that &lt;strong&gt;H3 cell
IDs across resolutions live in disjoint integer spaces.&lt;/strong&gt; The
encoding embeds the resolution in the ID itself, so a leaf cell at
res 9 and its res-5 ancestor are unrelated integers. There’s no
equivalent to S2’s &lt;code&gt;[range_min, range_max]&lt;/code&gt; that lets a single leaf
ID be range-checked against a cell at any coarser level. With H3
you can do lookups, but only at one &lt;em&gt;fixed&lt;/em&gt; resolution at a time.
&lt;strong&gt;S2’s strict quadtree invariant, that every cell at every level is
representable as an integer interval over leaf IDs, is what makes
the multi-resolution unified table work.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Concretely, here’s what that costs visually. The same patch of
Lombardia, covered two ways:&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/s2_vs_h3_lombardia.Db7VYosd.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1404&quot; height=&quot;705&quot; srcset=&quot;https://www.abahgat.com/_astro/s2_vs_h3_lombardia.Db7VYosd_20CLG6.webp 400w, https://www.abahgat.com/_astro/s2_vs_h3_lombardia.Db7VYosd_Z20vshk.webp 768w, https://www.abahgat.com/_astro/s2_vs_h3_lombardia.Db7VYosd_17DvY8.webp 1024w, https://www.abahgat.com/_astro/s2_vs_h3_lombardia.Db7VYosd_Y5d6A.webp 1404w, https://www.abahgat.com/_astro/s2_vs_h3_lombardia.Db7VYosd_Z1PxYKl.webp 2040w, https://www.abahgat.com/_astro/s2_vs_h3_lombardia.Db7VYosd_Z1TGbhN.webp 2808w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1404px; max-height: 705px; aspect-ratio: 1.9914893617021276; width: 100%;&quot; alt=&quot;Lombardia covered two ways. On the left, S2&apos;s mixed-level cell-union: 59 cells at levels 5–9. Three large green INTERIOR cells tile the polygon&apos;s bulk; the remaining 56 BOUNDARY cells (orange, finer) handle the edges. Mixing levels is legal because S2&apos;s quadtree guarantees each cell&apos;s [range_min, range_max] over leaf IDs cleanly contains all its descendants, so coarse and fine cells live in the same integer-indexed table. On the right, H3 at two resolutions overlaid: res 4 (15 large blue hexes) and res 5 (97 red strokes laid on top). The red boundaries cross the blue ones; the smaller hexes do not nest inside the bigger ones. A res-4 cell well inside Lombardia has a 14.3% symmetric-difference with the union of its 7 res-5 children: children spill out and leave gaps. That&apos;s why H3 can&apos;t do the S2 trick of mixing resolutions in one cell-union table.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Lombardia covered two ways. On the left, S2&apos;s mixed-level cell-union: 59 cells at levels 5–9. Three large green INTERIOR cells tile the polygon&apos;s bulk; the remaining 56 BOUNDARY cells (orange, finer) handle the edges. Mixing levels is legal because S2&apos;s quadtree guarantees each cell&apos;s [range_min, range_max] over leaf IDs cleanly contains all its descendants, so coarse and fine cells live in the same integer-indexed table. On the right, H3 at two resolutions overlaid: res 4 (15 large blue hexes) and res 5 (97 red strokes laid on top). The red boundaries cross the blue ones; the smaller hexes do not nest inside the bigger ones. A res-4 cell well inside Lombardia has a 14.3% symmetric-difference with the union of its 7 res-5 children: children spill out and leave gaps. That&apos;s why H3 can&apos;t do the S2 trick of mixing resolutions in one cell-union table. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;S2’s mixed-level union is a clean tiling: a handful of large interior
cells, a band of small boundary cells, no gaps and no overlaps. H3’s
geometry can’t deliver that: pick one resolution and you get a
sensible cover; try to mix two and the cell boundaries don’t agree.
The hex grid is the wrong shape for a strict quadtree invariant.&lt;/p&gt;
&lt;p&gt;Cell counts across the three nested admin polygons make the same
argument numerically:&lt;/p&gt;

































&lt;div style=&quot;overflow:auto&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;S2 (mixed levels)&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;H3 res 5 (~8.5 km)&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;H3 res 7 (~1.2 km)&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;H3 res 9 (~174 m)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Italia (country)&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;178&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;1,701&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;83,322&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;4,081,854&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Lombardia (region)&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;59&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;97&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;4,653&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;227,466&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bergamo (locality)&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;1&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;8&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;380&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The S2 union uses cells at levels 4–8 for Italy, 5–9 for Lombardia,
8–12 for Bergamo: bulk gets covered by a few large cells, only the
edges need fine ones, all coexisting in the same table because the
integer intervals at the leaf level let them. H3 has to commit to
one resolution: at res 5 Bergamo is a single hexagon (unresolvable),
at res 7 Italy needs 83,000 hexagons in the index, at res 9 the index
is unworkable. H3’s &lt;code&gt;compact()&lt;/code&gt; helps some (it can fold contiguous
fine cells back into parents) but the resulting set still isn’t a
range-comparison primitive, and the lookup remains set-membership at
a chosen resolution. &lt;strong&gt;You can build a working spatial join with
H3; you can’t build &lt;em&gt;this particular&lt;/em&gt; multi-scale unified-index
join with it.&lt;/strong&gt; That’s the structural property S2 gives you, and
the post is built around it.&lt;/p&gt;
&lt;p&gt;Concretely: the unified cell-union table over 572k admin polygons is
46 MB. The five radius-band cell-unions over 770k POIs total ~70 MB.
Both indexes are tiny, and building them is the only slow part: the
admin cell-unions take 36 minutes, the radius bands 30, so the indexes
are ready in &lt;strong&gt;about an hour&lt;/strong&gt;. Everything after that is a query. The
range-join that resolves country, region, and locality for all 770k
POIs returns in &lt;strong&gt;under a second&lt;/strong&gt;; classifying every one of the 567k
localities on Earth into a nearest-hospital band takes &lt;strong&gt;30 seconds&lt;/strong&gt;.
The one query-side step that costs minutes, the boundary refinement, is
geometry-loading, not the join.&lt;/p&gt;
&lt;p&gt;Which loops back to the opening. I used a distributed batch job via Flume for the original problem at Google, not because the algorithm needed one but because the data was huge. Here the entire pipeline (both indexes, the joins, the refinement, the aggregation) runs on a single CPU, no GPU, no cluster. A point-in-polygon question becomes an integer-interval
question, and the rest is sort-merge. Build the index once, and the
planet-scale query is seconds of work. The index is what did the real
work, at fleet scale and on a desktop alike.&lt;/p&gt;
&lt;h2 id=&quot;reproduce&quot;&gt;Reproduce&lt;/h2&gt;
&lt;p&gt;The code is at
&lt;a href=&quot;https://github.com/abahgat/s2-spatial-join&quot;&gt;github.com/abahgat/s2-spatial-join&lt;/a&gt;
(public, MIT-licensed). All data sources are open and auth-free.
End-to-end on a workstation, from raw Overture tiles to the finished
leaderboard, runs in under two hours. Almost all of that is the
one-time index build (the admin cell-unions and the radius-band cap
coverings, the latter dominated by the finest-level band), not the
queries, which answer in seconds once the index exists.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; sync&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; overturemaps&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; download&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -t&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; place&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;         -f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; geoparquet&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; data/cache/world_places.parquet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; overturemaps&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; download&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -t&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; division_area&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; geoparquet&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; data/cache/world_divisions.parquet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; python&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; scripts/bake_healthcare_world.py&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            # phases A–D: indexes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; python&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; scripts/bake_healthcare_world_geojson.py&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # phase E: aggregate + leaderboard&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;plf-root not-prose my-6&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;svg class=&quot;plf-svg&quot; viewBox=&quot;0 0 1130 440&quot; role=&quot;img&quot; aria-label=&quot;Data pipeline flow. Two open datasets, Overture places (770k healthcare POIs) and Overture divisions (572k admin polygons), feed S2 indexes. The same S2 primitive answers two questions: a containment track (admin cell-union, range-join in DuckDB yielding 5.6M candidates, boundary refinement to 4.24M confirmed pairs) and a distance track (five radius-band cell-unions, then classify each locality by its smallest band). Both converge into aggregation, producing the GeoJSON and leaderboard.&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;defs data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;marker id=&quot;plf-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; class=&quot;plf-arrowhead&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/path&gt; &lt;/marker&gt; &lt;/defs&gt; &lt;!-- Lane backgrounds + labels --&gt; &lt;rect class=&quot;plf-lane plf-lane-c&quot; x=&quot;236&quot; y=&quot;44&quot; width=&quot;688&quot; height=&quot;178&quot; rx=&quot;12&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;rect class=&quot;plf-lane plf-lane-d&quot; x=&quot;236&quot; y=&quot;288&quot; width=&quot;458&quot; height=&quot;104&quot; rx=&quot;12&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;plf-lane-label plf-label-c&quot; x=&quot;252&quot; y=&quot;38&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; CONTAINMENT · which admin units contain each POI &lt;/text&gt; &lt;text class=&quot;plf-lane-label plf-label-d&quot; x=&quot;252&quot; y=&quot;410&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; DISTANCE · nearest hospital per locality &lt;/text&gt; &lt;!-- Edges --&gt; &lt;line class=&quot;plf-edge&quot; x1=&quot;200&quot; y1=&quot;279&quot; x2=&quot;254&quot; y2=&quot;103&quot; marker-end=&quot;url(#plf-arrow)&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/line&gt;&lt;line class=&quot;plf-edge&quot; x1=&quot;200&quot; y1=&quot;179&quot; x2=&quot;476&quot; y2=&quot;166&quot; marker-end=&quot;url(#plf-arrow)&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/line&gt;&lt;line class=&quot;plf-edge&quot; x1=&quot;200&quot; y1=&quot;179&quot; x2=&quot;254&quot; y2=&quot;340&quot; marker-end=&quot;url(#plf-arrow)&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/line&gt;&lt;line class=&quot;plf-edge&quot; x1=&quot;430&quot; y1=&quot;103&quot; x2=&quot;476&quot; y2=&quot;166&quot; marker-end=&quot;url(#plf-arrow)&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/line&gt;&lt;line class=&quot;plf-edge&quot; x1=&quot;676&quot; y1=&quot;166&quot; x2=&quot;716&quot; y2=&quot;168&quot; marker-end=&quot;url(#plf-arrow)&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/line&gt;&lt;line class=&quot;plf-edge&quot; x1=&quot;430&quot; y1=&quot;340&quot; x2=&quot;476&quot; y2=&quot;340&quot; marker-end=&quot;url(#plf-arrow)&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/line&gt;&lt;line class=&quot;plf-edge&quot; x1=&quot;906&quot; y1=&quot;168&quot; x2=&quot;946&quot; y2=&quot;260&quot; marker-end=&quot;url(#plf-arrow)&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/line&gt;&lt;line class=&quot;plf-edge&quot; x1=&quot;676&quot; y1=&quot;340&quot; x2=&quot;946&quot; y2=&quot;260&quot; marker-end=&quot;url(#plf-arrow)&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/line&gt; &lt;!-- Nodes --&gt; &lt;g class=&quot;plf-node plf-input&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect x=&quot;24&quot; y=&quot;150&quot; width=&quot;176&quot; height=&quot;58&quot; rx=&quot;8&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt;  &lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;112&quot; y=&quot;171.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; Overture places &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-muted&quot; x=&quot;112&quot; y=&quot;186.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; 770k healthcare POIs &lt;/text&gt; &lt;/g&gt;&lt;g class=&quot;plf-node plf-input&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect x=&quot;24&quot; y=&quot;250&quot; width=&quot;176&quot; height=&quot;58&quot; rx=&quot;8&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt;  &lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;112&quot; y=&quot;271.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; Overture divisions &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-muted&quot; x=&quot;112&quot; y=&quot;286.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; 572k admin polygons &lt;/text&gt; &lt;/g&gt;&lt;g class=&quot;plf-node plf-containment&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect x=&quot;254&quot; y=&quot;70&quot; width=&quot;176&quot; height=&quot;66&quot; rx=&quot;8&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;g data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect class=&quot;plf-tag&quot; x=&quot;262&quot; y=&quot;61&quot; width=&quot;18&quot; height=&quot;18&quot; rx=&quot;9&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;plf-tag-text&quot; x=&quot;271&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; A &lt;/text&gt; &lt;/g&gt; &lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;342&quot; y=&quot;95.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; Admin S2 cell-union &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-muted&quot; x=&quot;342&quot; y=&quot;110.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; 572k polys · 46 MB &lt;/text&gt; &lt;/g&gt;&lt;g class=&quot;plf-node plf-containment&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect x=&quot;476&quot; y=&quot;120&quot; width=&quot;200&quot; height=&quot;92&quot; rx=&quot;8&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;g data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect class=&quot;plf-tag&quot; x=&quot;484&quot; y=&quot;111&quot; width=&quot;18&quot; height=&quot;18&quot; rx=&quot;9&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;plf-tag-text&quot; x=&quot;493&quot; y=&quot;120&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; B &lt;/text&gt; &lt;/g&gt; &lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;576&quot; y=&quot;151&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; Range-join · DuckDB &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;576&quot; y=&quot;166&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; leaf ∈ [min, max] &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-muted&quot; x=&quot;576&quot; y=&quot;181&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; 5.6M candidates · &amp;lt;1 s &lt;/text&gt; &lt;/g&gt;&lt;g class=&quot;plf-node plf-containment&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect x=&quot;716&quot; y=&quot;132&quot; width=&quot;190&quot; height=&quot;72&quot; rx=&quot;8&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;g data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect class=&quot;plf-tag&quot; x=&quot;724&quot; y=&quot;123&quot; width=&quot;18&quot; height=&quot;18&quot; rx=&quot;9&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;plf-tag-text&quot; x=&quot;733&quot; y=&quot;132&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; C &lt;/text&gt; &lt;/g&gt; &lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;811&quot; y=&quot;153&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; Boundary refine &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;811&quot; y=&quot;168&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; shapely · 14% of POIs &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-muted&quot; x=&quot;811&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; 4.24M pairs · 6 min &lt;/text&gt; &lt;/g&gt;&lt;g class=&quot;plf-node plf-distance&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect x=&quot;254&quot; y=&quot;300&quot; width=&quot;176&quot; height=&quot;80&quot; rx=&quot;8&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;g data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect class=&quot;plf-tag&quot; x=&quot;262&quot; y=&quot;291&quot; width=&quot;18&quot; height=&quot;18&quot; rx=&quot;9&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;plf-tag-text&quot; x=&quot;271&quot; y=&quot;300&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; D &lt;/text&gt; &lt;/g&gt; &lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;342&quot; y=&quot;325&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; Radius-band cell-unions &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;342&quot; y=&quot;340&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; 1 / 5 / 15 / 30 / 100 km &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-muted&quot; x=&quot;342&quot; y=&quot;355&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; 70 MB · 30 min &lt;/text&gt; &lt;/g&gt;&lt;g class=&quot;plf-node plf-distance&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect x=&quot;476&quot; y=&quot;308&quot; width=&quot;200&quot; height=&quot;64&quot; rx=&quot;8&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;g data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect class=&quot;plf-tag&quot; x=&quot;484&quot; y=&quot;299&quot; width=&quot;18&quot; height=&quot;18&quot; rx=&quot;9&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;plf-tag-text&quot; x=&quot;493&quot; y=&quot;308&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; D &lt;/text&gt; &lt;/g&gt; &lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;576&quot; y=&quot;332.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; Classify each locality &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-muted&quot; x=&quot;576&quot; y=&quot;347.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; by smallest band &lt;/text&gt; &lt;/g&gt;&lt;g class=&quot;plf-node plf-output&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect x=&quot;946&quot; y=&quot;216&quot; width=&quot;160&quot; height=&quot;88&quot; rx=&quot;8&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;g data-astro-cid-55q7lj45=&quot;&quot;&gt; &lt;rect class=&quot;plf-tag&quot; x=&quot;954&quot; y=&quot;207&quot; width=&quot;18&quot; height=&quot;18&quot; rx=&quot;9&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;&lt;/rect&gt; &lt;text class=&quot;plf-tag-text&quot; x=&quot;963&quot; y=&quot;216&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; E &lt;/text&gt; &lt;/g&gt; &lt;text class=&quot;plf-line plf-line-head&quot; x=&quot;1026&quot; y=&quot;252.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; Aggregate per locality &lt;/text&gt;&lt;text class=&quot;plf-line plf-line-muted&quot; x=&quot;1026&quot; y=&quot;267.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt; GeoJSON + leaderboard &lt;/text&gt; &lt;/g&gt; &lt;text class=&quot;plf-foot&quot; x=&quot;565&quot; y=&quot;432&quot; text-anchor=&quot;middle&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;
phases A–D: scripts/bake_healthcare_world.py · phase E: …_geojson.py · one S2 primitive, two questions
&lt;/text&gt; &lt;/svg&gt; &lt;figcaption class=&quot;plf-caption&quot; data-astro-cid-55q7lj45=&quot;&quot;&gt;The whole pipeline. Two open Overture datasets feed one S2 index, and the same range-comparison primitive answers both questions: containment (which country, region, and locality contain each POI) and distance (the nearest hospital per locality, via radius-band cell-unions). Both tracks converge into the per-locality aggregate that becomes the leaderboard. Phase tags A–E match the script comments above.&lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;If you want to ask the same kind of question of a different POI
category (&lt;em&gt;most isolated locality from a school, a
pharmacy, a fire station, a bookstore, a coffee shop&lt;/em&gt;) the pipeline
generalizes by changing one filter line, and runs in the same amount of
time. The S2 index doesn’t care what kind of POI you put in it.
That, more than any specific number, is the point.&lt;/p&gt;
&lt;p&gt;The engineering leader I was coaching: their pipeline is now an
integer-interval check too. They didn’t need a cluster either.&lt;/p&gt;</content:encoded></item><item><title>Same Agent, Different Score: The Problem With Testing Non-Deterministic AI</title><link>https://www.abahgat.com/blog/same-agent-different-score</link><guid isPermaLink="true">https://www.abahgat.com/blog/same-agent-different-score</guid><description>Before building tools for my Zork-playing agents, I needed a benchmark I could trust. I ran five local models through fifty playthroughs and discovered that the same model can score 40 or 0 on the same game. Getting honest numbers required three harness versions, structured telemetry, and a loop detector that learned the difference between stuck and thorough.</description><pubDate>Thu, 16 Apr 2026 14:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.abahgat.com/stuck-in-the-maze&quot;&gt;A few days ago&lt;/a&gt; I experimented with a couple local AI models by having them play Zork. They both got stuck in the maze, and the results were interesting: while one agent managed to score 35 points on a good run, most runs scored zero. The next steps in my plan was to give the agent structured tools such as maps, memory and breadcrumbs, and then test how they affected gameplay.&lt;/p&gt;
&lt;p&gt;Before building tools and trying to measure their impact, I wanted a solid foundation to build on. That meant picking a model, but more importantly it meant having a benchmark I trusted. I expect finding the right approach with tools will require dozens of design decisions, and I need to be able to tell whether each one actually helps. Zork is an ideal testbed for getting evaluation right: runs are cheap (minutes, not hours), the score is unambiguous (the game tracks it for you), and every turn is logged, so you can replay exactly what happened.&lt;/p&gt;
&lt;p&gt;I expected the work to be in running models and comparing scores. Most of it turned out to be in getting the ruler right. Even though I read a lot about evals in the context of applied AI to have a baseline expectation, nothing beats first-hand experience.&lt;/p&gt;
&lt;p&gt;By now, everyone has heard remarks that LLM outputs are non-deterministic, to the point that it’s become a hand-wave: “results may vary.” However, when you actually try to make decisions based on aggregate scores from dozens of runs, the non-determinism stops being a footnote and starts being more tangible. The same model can score 40 on one run and 0 on the next. Different benchmark harnesses can make the same model look good or terrible depending on how they handle edge cases. And the only way to tell whether your numbers mean anything is to invest in the telemetry to audit them after the fact.&lt;/p&gt;
&lt;p&gt;One caveat before the numbers: Zork has been on the internet for decades by now, and I bet at least some of these models have seen walkthroughs in training. I’m not expecting this experiment to measure general ability at playing text adventure games from a blank slate. What it measures is whether an agent can execute on a multi-step strategy it plausibly already has a model of, despite the non-determinism. That’s even more important for the tool-use experiments I intend to run next.&lt;/p&gt;
&lt;h2 id=&quot;two-performance-tiers&quot;&gt;Two performance tiers&lt;/h2&gt;
&lt;p&gt;I expanded from two models to five, all running locally on the same RTX 5080 with 16GB VRAM. Each model played five independent games capped at 100 turns.&lt;/p&gt;















































&lt;div style=&quot;overflow:auto&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Model&lt;/th&gt;&lt;th&gt;Params&lt;/th&gt;&lt;th&gt;Mean Score&lt;/th&gt;&lt;th&gt;Avg Latency&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Gemma 4 26B&lt;/td&gt;&lt;td&gt;26B MoE (4B active)&lt;/td&gt;&lt;td&gt;19.0 (±16.4)&lt;/td&gt;&lt;td&gt;6.5s/turn&lt;/td&gt;&lt;td&gt;Highest peaks, highest variance&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mistral Small 24B&lt;/td&gt;&lt;td&gt;24B dense&lt;/td&gt;&lt;td&gt;12.0 (±11.0)&lt;/td&gt;&lt;td&gt;2.9s/turn&lt;/td&gt;&lt;td&gt;More consistent&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Qwen 2.5 14B&lt;/td&gt;&lt;td&gt;14B dense&lt;/td&gt;&lt;td&gt;3.0 (±6.7)&lt;/td&gt;&lt;td&gt;0.9s/turn&lt;/td&gt;&lt;td&gt;Fast but directionless&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Gemma 4 E4B&lt;/td&gt;&lt;td&gt;8B total (4B active)&lt;/td&gt;&lt;td&gt;0.0 (±3.5)&lt;/td&gt;&lt;td&gt;4.8s/turn&lt;/td&gt;&lt;td&gt;Too small&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Phi-4 Reasoning 14B&lt;/td&gt;&lt;td&gt;14B dense&lt;/td&gt;&lt;td&gt;0.0 (±0.0)&lt;/td&gt;&lt;td&gt;5.4s/turn&lt;/td&gt;&lt;td&gt;Couldn’t follow the format&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/score_vs_latency.D5M-lXim.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1547&quot; height=&quot;827&quot; srcset=&quot;https://www.abahgat.com/_astro/score_vs_latency.D5M-lXim_H6JTM.webp 400w, https://www.abahgat.com/_astro/score_vs_latency.D5M-lXim_ZF4Q7R.webp 768w, https://www.abahgat.com/_astro/score_vs_latency.D5M-lXim_BLRlw.webp 1024w, https://www.abahgat.com/_astro/score_vs_latency.D5M-lXim_ZCzpNk.webp 1547w, https://www.abahgat.com/_astro/score_vs_latency.D5M-lXim_ges65.webp 2040w, https://www.abahgat.com/_astro/score_vs_latency.D5M-lXim_Z1aJh5J.webp 3094w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1547px; max-height: 827px; aspect-ratio: 1.8706166868198306; width: 100%;&quot; alt=&quot;Score vs latency tradeoff across all five models. The stars mark model means; the dashed line shows the Pareto frontier.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Score vs latency tradeoff across all five models. The stars mark model means; the dashed line shows the Pareto frontier. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;The first clear result: there’s no smooth spectrum. Three models score near zero, two score in the double digits. There’s nothing in between. This was already a win, because at this point I didn’t know whether I should expect a large gap in capabilities, and seeing statistically significant differences allowed me to start making decisions.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/strip_plots.DSfwdjo9.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1658&quot; height=&quot;1182&quot; srcset=&quot;https://www.abahgat.com/_astro/strip_plots.DSfwdjo9_ZDd8vQ.webp 400w, https://www.abahgat.com/_astro/strip_plots.DSfwdjo9_Z1MFuH4.webp 768w, https://www.abahgat.com/_astro/strip_plots.DSfwdjo9_JkACY.webp 1024w, https://www.abahgat.com/_astro/strip_plots.DSfwdjo9_Zuz0Aj.webp 1658w, https://www.abahgat.com/_astro/strip_plots.DSfwdjo9_Fh23L.webp 2040w, https://www.abahgat.com/_astro/strip_plots.DSfwdjo9_2gQa91.webp 3316w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1658px; max-height: 1182px; aspect-ratio: 1.4027072758037225; width: 100%;&quot; alt=&quot;Individual runs as dots, with mean and 95% CI bars. The gap between the top two models and the bottom three is not a gradient. It&apos;s a cliff.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Individual runs as dots, with mean and 95% CI bars. The gap between the top two models and the bottom three is not a gradient. It&apos;s a cliff. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;This seems to be one of the few results that survives the noise, and hints to a capability cliff. Gemma 4 E4B has one-third the parameters of its 26B sibling and drops from solving the cellar to scoring zero. Phi-4 Reasoning can’t even follow the JSON output format: every run looped within six turns. The gap between a model that “can play Zork” and one that “can’t” is significant, and it’s the only comparison where n=5 gives you statistical confidence.&lt;/p&gt;
&lt;p&gt;The gap between the two models that &lt;em&gt;can&lt;/em&gt; play? That’s where things get complicated.&lt;/p&gt;
&lt;h2 id=&quot;the-variance-is-not-noise&quot;&gt;The variance is not noise&lt;/h2&gt;
&lt;p&gt;Look at Gemma’s individual scores: [25, 25, 0, 40, 5]. At first I thought that was just noise in measurement. When I started reading the game traces, it looked more like the model taking genuinely different strategic paths on each run.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/run_clusters.B6mdxSOH.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1667&quot; height=&quot;947&quot; srcset=&quot;https://www.abahgat.com/_astro/run_clusters.B6mdxSOH_ilu5m.webp 400w, https://www.abahgat.com/_astro/run_clusters.B6mdxSOH_4J8Fz.webp 768w, https://www.abahgat.com/_astro/run_clusters.B6mdxSOH_Z219Eu0.webp 1024w, https://www.abahgat.com/_astro/run_clusters.B6mdxSOH_yzTMt.webp 1667w, https://www.abahgat.com/_astro/run_clusters.B6mdxSOH_juyk9.webp 2040w, https://www.abahgat.com/_astro/run_clusters.B6mdxSOH_SMBpm.webp 3334w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1667px; max-height: 947px; aspect-ratio: 1.7602956705385429; width: 100%;&quot; alt=&quot;Each point is a single run. The stars mark model means, the ellipses show spread. Gemma clusters around high-score/short-lived; Mistral clusters around moderate-score/long-lived.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Each point is a single run. The stars mark model means, the ellipses show spread. Gemma clusters around high-score/short-lived; Mistral clusters around moderate-score/long-lived. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Gemma plays aggressively.&lt;/strong&gt; It pushes into the cellar early, grabs items, and takes risks. When it works, it scores fast: 40 points in 48 turns in its best run. But it died in 80% of its runs, often in the dark or by falling into a pit. One run never even entered the house.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mistral plays cautiously.&lt;/strong&gt; It explores methodically, backtracks when confused, and survives much longer. But it plateaus. Its weakest runs scored 5 because it wandered the forest without finding a way in.&lt;/p&gt;
&lt;p&gt;These aren’t noisy samples from the same distribution. They’re different behaviors sampled from different regions of the model’s probability space. Back when I was still struggling with getting Qwen to stick with English, I had set temperature to 0.2: low, but not zero. That small amount of randomness compounds across dozens of decisions into completely divergent playthroughs. This is aleatoric uncertainty: randomness inherent in the system, not reducible by collecting more data. More runs don’t make the distribution narrower. They reveal its shape, which is the actual information.&lt;/p&gt;
&lt;p&gt;Sam Savage calls this the &lt;a href=&quot;https://hbr.org/2002/11/the-flaw-of-averages&quot;&gt;“flaw of averages”&lt;/a&gt;: plans based on average conditions are wrong on average, because the mean of a non-linear system doesn’t represent any actual outcome. Gemma’s mean of 19 is a score no individual run ever produced. It doesn’t tell you that Gemma either scored 25+ or crashed. Mistral’s mean of 12 doesn’t tell you that it rarely hit those highs but also rarely hit zero. The distribution &lt;em&gt;is the result&lt;/em&gt;, not a nuisance to be averaged away.&lt;/p&gt;
&lt;p&gt;If you’re choosing between them as agents, this matters. A cautious agent that reliably scores 15 is a very different tool than an aggressive one that scores 40 or 0, even if they average out similarly. For traditional evals, where the goal is to match a known expected answer, variance is noise you want to minimize. For agents navigating a complex space with branching consequences, variance is signal. It tells you the shape of what the agent might do, and that shape determines what kind of help it needs. That’s why &lt;code&gt;pass@k&lt;/code&gt; metrics are valuable.&lt;/p&gt;
&lt;p&gt;Put differently: if you only get one shot at a problem, you want Mistral. Its worst run was a 5, not a 0. If you can run the agent several times and keep the best result, you want Gemma: its ceiling is much higher even though most individual runs fail. The mean hides this entirely.&lt;/p&gt;
&lt;p&gt;For deployments in the real world, these are the same two failure modes I have to consider. One agent type moves fast and occasionally nails it, but blows up when a step goes wrong. The other is so careful it runs out of budget before making progress. The tools you’d build to help them are different: the aggressive agent needs guardrails, the cautious one needs a push.&lt;/p&gt;
&lt;h2 id=&quot;three-tries-at-honest-measurement&quot;&gt;Three tries at honest measurement&lt;/h2&gt;
&lt;p&gt;This is where the deliberate investment in benchmarking paid for itself. I iterated through three versions of the harness, and the scores changed dramatically each time. And it wasn’t because the models changed, it was because of changes in the harness and benchmarking guardrails.&lt;/p&gt;
&lt;h3 id=&quot;v1-too-lenient&quot;&gt;v1: Too lenient&lt;/h3&gt;
&lt;p&gt;The original harness only detected exact action loops: &lt;code&gt;open mailbox, close mailbox, open mailbox, close mailbox&lt;/code&gt;. A model bouncing between the kitchen and the living room with different actions each time could wander for 80+ turns undetected. When the API timed out, the harness injected a fake action and kept going, corrupting game state. Under v1, Mistral looked strong (mean 19.5 across two batches of n=5) because its methodical wandering was never caught; Gemma’s mean came in at 13.0, dragged down by a catastrophic -10 run where injected actions corrupted the game state beyond recovery. Both numbers were the harness talking, not the models.&lt;/p&gt;
&lt;h3 id=&quot;v2-overcorrected&quot;&gt;v2: Overcorrected&lt;/h3&gt;
&lt;p&gt;I added location-based loop detection, a token cap, and discarded runs on consecutive API errors. Scores cratered. Gemma dropped to 5.0, Mistral to 7.0. I initially took this as confirmation that v1 had been inflated.&lt;/p&gt;
&lt;p&gt;But here’s the thing about non-deterministic evaluation: &lt;strong&gt;when your scores change, you don’t know if the system got worse or your ruler got shorter.&lt;/strong&gt; I needed to look at individual runs to tell the difference.&lt;/p&gt;
&lt;h3 id=&quot;the-telemetry-payoff&quot;&gt;The telemetry payoff&lt;/h3&gt;
&lt;p&gt;This is where an investment I’d made early on paid off. From the start, every turn logged the action taken, the location, the score, the model’s reasoning, and the latency, and some hardware statistics. I hadn’t needed most of this data. The benchmark “worked.” The charts looked reasonable. But now I could replay every termination decision.&lt;/p&gt;
&lt;p&gt;Two of Gemma’s five v2 terminations were false positives. Both happened in the Kitchen. In both cases, the model had just scored points by entering the room, then systematically picked up every item: take bottle, take sack, open sack, take garlic, take food. Six turns, six unique actions, all productive. But the location detector saw “Kitchen” six times in a row and killed the run. It couldn’t distinguish “stuck in a room” from “thoroughly looting a room.”&lt;/p&gt;
&lt;p&gt;I diagnosed this entirely from logs, without re-running a single game. If I’d been looking only at aggregate scores, I would have concluded that v1 scores were inflated and v2 was the honest version. The per-turn telemetry told a different story: v2 was punishing exactly the behavior I wanted to encourage. I keep coming back to this: you can’t tell whether a benchmark change is a fix or a regression from aggregate numbers alone. You need per-unit data or you’re guessing.&lt;/p&gt;
&lt;h3 id=&quot;v3-stuck-vs-thorough&quot;&gt;v3: Stuck vs. thorough&lt;/h3&gt;
&lt;p&gt;The fix was straightforward: before firing the location loop detector, check whether the actions are diverse. If at least two-thirds of recent actions are unique, the model is interacting with the environment, not stuck. This preserved all four legitimate terminations while sparing the two false positives.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/v1_vs_v2.D_DPnJuL.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1908&quot; height=&quot;594&quot; srcset=&quot;https://www.abahgat.com/_astro/v1_vs_v2.D_DPnJuL_eP32S.webp 400w, https://www.abahgat.com/_astro/v1_vs_v2.D_DPnJuL_2q2Nav.webp 768w, https://www.abahgat.com/_astro/v1_vs_v2.D_DPnJuL_Z103qCG.webp 1024w, https://www.abahgat.com/_astro/v1_vs_v2.D_DPnJuL_dax1O.webp 1908w, https://www.abahgat.com/_astro/v1_vs_v2.D_DPnJuL_Z1nm4qL.webp 2040w, https://www.abahgat.com/_astro/v1_vs_v2.D_DPnJuL_W3nVk.webp 3816w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1908px; max-height: 594px; aspect-ratio: 3.212121212121212; width: 100%;&quot; alt=&quot;Harness evolution across three versions. v1 was too lenient, v2 overcorrected, v3 refined the loop detector.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Harness evolution across three versions. v1 was too lenient, v2 overcorrected, v3 refined the loop detector. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;Under v3, Gemma recovered to 19.0 and Mistral to 12.0. They still aren’t statistically different: the variance is too high with n=5. But I trust the scores now reflect actual gameplay, not artifacts of the harness.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/score_progression.B8zbeH5e.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;2508&quot; height=&quot;726&quot; srcset=&quot;https://www.abahgat.com/_astro/score_progression.B8zbeH5e_Z1fKcAF.webp 400w, https://www.abahgat.com/_astro/score_progression.B8zbeH5e_ZUAtyK.webp 768w, https://www.abahgat.com/_astro/score_progression.B8zbeH5e_Zp3eIq.webp 1024w, https://www.abahgat.com/_astro/score_progression.B8zbeH5e_ZPYToW.webp 2040w, https://www.abahgat.com/_astro/score_progression.B8zbeH5e_ZGlq76.webp 2508w, https://www.abahgat.com/_astro/score_progression.B8zbeH5e_Z1wzBaz.webp 5016w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 2508px; max-height: 726px; aspect-ratio: 3.4545454545454546; width: 100%;&quot; alt=&quot;Score progression across harness versions. v1 runs drift aimlessly, v2 cuts them short, v3 lets productive play continue.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Score progression across harness versions. v1 runs drift aimlessly, v2 cuts them short, v3 lets productive play continue. &lt;/figcaption&gt; &lt;/figure&gt;























&lt;div style=&quot;overflow:auto&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;v1 (lenient)&lt;/th&gt;&lt;th&gt;v2 (overcorrected)&lt;/th&gt;&lt;th&gt;v3 (refined)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Gemma 4 26B&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;13.0 ± 16.8 (n=5)&lt;/td&gt;&lt;td&gt;5.0 ± 7.1&lt;/td&gt;&lt;td&gt;19.0 ± 16.4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Mistral Small 24B&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;19.5 ± 10.9 (n=10)&lt;/td&gt;&lt;td&gt;7.0 ± 4.5&lt;/td&gt;&lt;td&gt;12.0 ± 11.0&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The v1-to-v3 journey taught me something I keep relearning: the hard part of evaluating non-deterministic agents isn’t running them. It’s defining what counts as progress and what counts as being stuck. “Is the agent doing different things?” isn’t enough — it might be doing different things in a circle. “Is the agent in the same place?” isn’t enough — it might be productively working a complex room. You need both signals, and you need the telemetry to check your assumptions after the fact.&lt;/p&gt;
&lt;h2 id=&quot;a-note-on-the-hardware-cliff&quot;&gt;A note on the hardware cliff&lt;/h2&gt;
&lt;p&gt;One thing that shaped the experiment more than I expected: I run everything on WSL2 with an RTX 5080 (16GB VRAM), and WSL’s Hyper-V overhead eats about 1.5 GB. That doesn’t sound like much, but open-source models are released in size tiers designed to fit common GPU memory boundaries (8GB, 16GB, 24GB). Being just below a boundary doesn’t get you a slightly slower model. It drops you to the next tier down, which may not be capable enough for your task. The capability cliff from earlier can be caused by 1.5 GB of VRAM overhead just as easily as by model architecture. I have more to say about this than I can fit here, but the short version: “run it locally” sounds simple until your model choice is dictated by a hypervisor you didn’t choose.&lt;/p&gt;
&lt;h2 id=&quot;the-troll-is-still-the-ceiling&quot;&gt;The Troll is still the ceiling&lt;/h2&gt;
&lt;p&gt;Across every model, every run, every harness version, one fact hasn’t changed: no agent has ever fought the Troll.&lt;/p&gt;
&lt;p&gt;The Troll guards the passage between the early game and the mid game. It requires selecting the right weapon from your inventory and attacking. Only one Gemma run, out of all the Gemma and Mistral playthroughs across every harness version, even reached the Maze beyond the Troll’s room. Every other run’s ceiling was the cellar.&lt;/p&gt;
&lt;p&gt;The lantern is the key gate. Runs that grab it and descend into the cellar consistently score 25 or more. Runs that don’t top out around 15. Both capable models can reach the cellar. Neither has gotten past it.&lt;/p&gt;
&lt;p&gt;This is where tools should finally make the difference. The Troll isn’t a reasoning problem. It’s an inventory management problem: the model needs to know it has a weapon, that the weapon is effective, and that it should fight instead of flee. That’s exactly the kind of contextual nudge that tool-assisted play can provide.&lt;/p&gt;
&lt;h2 id=&quot;what-i-learned-about-evaluating-agents&quot;&gt;What I learned about evaluating agents&lt;/h2&gt;
&lt;p&gt;This is why I invested in benchmarking before building tools. Getting evaluation wrong at this stage would have meant building the entirety of tools and harness on top of numbers I couldn’t trust. Zork made the problems visible early and cheaply: a three-minute run instead of a three-hour production incident. If I had to distill what I learned into advice for anyone evaluating non-deterministic AI systems:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Run more than once.&lt;/strong&gt; A single run tells you almost nothing. Gemma’s best run scored 40. Its worst scored 0. A one-shot eval would have given me either number and I’d have believed it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Look at distributions, not means.&lt;/strong&gt; The mean hides whether your agent is reliably mediocre or bimodally brilliant-and-catastrophic. For agents in complex environments, the distribution is the result: it tells you what the agent might do, and that determines what kind of help it needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your harness is part of the system.&lt;/strong&gt; The benchmark isn’t a neutral observer. Its loop detector, timeout policy, and fallback behavior all shape the scores. When results change, check whether the system changed or the ruler did. The Kitchen false positives would have led me to the wrong conclusion twice over if I hadn’t been able to replay individual turns.&lt;/p&gt;
&lt;p&gt;Hamel Husain and Gergely Orosz &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/evals&quot;&gt;make a similar argument&lt;/a&gt; from the enterprise-LLM side: the only way to know whether your evals are working is to read individual traces. I arrived at the same conclusion from a game-playing benchmark, which I take as evidence the problem isn’t domain-specific.&lt;/p&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;/h2&gt;
&lt;p&gt;I now have a benchmark I trust (though it took three tries) and two viable baseline models. Mistral Small at 2.9 seconds per turn gives me faster iteration; Gemma 4 at 6.5 seconds per turn has the higher ceiling. For building the ADK harness, I’ll use both: Mistral for rapid feedback and Gemma for measuring whether tools actually move the needle.&lt;/p&gt;
&lt;p&gt;The question is straightforward: does tool access change the scores? The Python benchmark stays as the control. The honest harness will tell me.&lt;/p&gt;
&lt;p&gt;Either way, the Troll is still waiting.&lt;/p&gt;</content:encoded></item><item><title>Stuck in the Maze: Why AI Agents Can&apos;t Hold the Map</title><link>https://www.abahgat.com/blog/stuck-in-the-maze</link><guid isPermaLink="true">https://www.abahgat.com/blog/stuck-in-the-maze</guid><description>I had local AI models play Zork, the 1981 text adventure, to study why agents struggle to navigate connected systems. One started responding in Thai. Most scored zero. All got hopelessly stuck in the maze. What broke says a lot about why agents get lost in microservices too.</description><pubDate>Tue, 07 Apr 2026 02:56:00 GMT</pubDate><content:encoded>&lt;p&gt;I was testing a local AI model this weekend when it started responding in Thai.&lt;/p&gt;
&lt;p&gt;Not gibberish. Actual Thai script, mixed with Chinese characters. I’d asked it to play Zork, the 1981 text adventure, and it was doing everything except that.&lt;/p&gt;
&lt;p&gt;This wasn’t what I set out to study. At work, I’ve had good results getting AI agents to respond to cloud alerts. A service throws an error, the agent reads the logs, traces the relevant code, and proposes a fix. But when a fix requires tracing a request from service A through a message queue to service B, then to service C’s database, the agent often gets lost. Not because it can’t reason about each piece. It can reason remarkably well about individual pieces in isolation. It just can’t hold the map.&lt;/p&gt;
&lt;p&gt;I wanted to study that limitation in isolation. No pixels, no distributed systems, no production risk. Inspired by &lt;a href=&quot;https://labs.ramp.com/rct&quot;&gt;Ramp’s experiment getting Claude to play RollerCoaster Tycoon&lt;/a&gt;, I picked the simplest possible test of “can an agent find its way around?”: a text adventure.&lt;/p&gt;
&lt;p&gt;I also suspected that small local models would struggle with this far more than frontier reasoning models. That made the experiment more interesting, not less: if tools and scaffolding are what let frontier models succeed, then small models are the best stress test for whether your harness is doing its job.&lt;/p&gt;
&lt;h2 id=&quot;the-setup&quot;&gt;The setup&lt;/h2&gt;
&lt;p&gt;Zork drops you in front of a white house. You explore rooms, collect items, solve puzzles, and try to score 350 points. The world is a graph of interconnected locations with descriptions, objects, and a few characters. It’s played entirely through typed commands like &lt;code&gt;go north&lt;/code&gt;, &lt;code&gt;take lantern&lt;/code&gt;, and &lt;code&gt;open trapdoor&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I wired together &lt;a href=&quot;https://github.com/microsoft/jericho&quot;&gt;Jericho&lt;/a&gt; (Microsoft’s Python library that runs the original game file and exposes the state machine) with &lt;a href=&quot;https://github.com/mariozechner/pi-coding-agent&quot;&gt;Pi Coding Agent&lt;/a&gt;, a TypeScript-based agent framework. Ollama running on an RTX 5080 with 16GB VRAM provided the model, and a custom bridge connected everything together, validating actions against Jericho’s state and logging every turn. I tested two models: Qwen 2.5 14B and Gemma 4 26B.&lt;/p&gt;
&lt;p&gt;The goal was simple: tell the agent to play Zork. No hand-holding, no hints. Just: “you are an autonomous explorer, play the game.”&lt;/p&gt;
&lt;h2 id=&quot;day-1-why-is-my-agent-speaking-thai&quot;&gt;Day 1: why is my agent speaking Thai?&lt;/h2&gt;
&lt;p&gt;The first attempt was with Qwen 2.5 14B. I gave it a system prompt explaining it was an autonomous Zork player, handed it a tool to send commands to the game, and let it run.&lt;/p&gt;
&lt;p&gt;It immediately broke character. Instead of playing, it started explaining how text adventures work. “In Zork, you typically want to explore your surroundings by using commands like LOOK and EXAMINE…” The model has been trained so aggressively on being a conversational assistant that it defaults to helping you play, rather than playing itself.&lt;/p&gt;
&lt;p&gt;Fine. I tightened the constraints. Strict system prompt: “You are the autonomous player. Do not speak to me. Execute moves only. English only.”&lt;/p&gt;
&lt;p&gt;That’s when it started outputting Thai.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/wrong_language.BdRiqn-0.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1734&quot; height=&quot;1271&quot; srcset=&quot;https://www.abahgat.com/_astro/wrong_language.BdRiqn-0_1xbtTj.webp 400w, https://www.abahgat.com/_astro/wrong_language.BdRiqn-0_ZBCo8r.webp 768w, https://www.abahgat.com/_astro/wrong_language.BdRiqn-0_2rzK52.webp 1024w, https://www.abahgat.com/_astro/wrong_language.BdRiqn-0_1OJsg6.webp 1734w, https://www.abahgat.com/_astro/wrong_language.BdRiqn-0_Z2hzD0S.webp 2040w, https://www.abahgat.com/_astro/wrong_language.BdRiqn-0_2k4nDI.webp 3468w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1734px; max-height: 1271px; aspect-ratio: 1.3642800944138473; width: 100%;&quot; alt=&quot;Qwen responding in Thai and Chinese instead of playing Zork&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Qwen responding in Thai and Chinese instead of playing Zork &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;Actual Thai script, interspersed with Chinese characters. Fragments like &lt;code&gt;推进完毕&lt;/code&gt; (roughly: “progress complete”). Under heavy “no chitchat” constraints, the model was reaching for high-probability tokens outside English. Qwen’s multilingual training means that when you suppress its English conversational patterns hard enough, other languages become the path of least resistance.&lt;/p&gt;
&lt;p&gt;This wasn’t random hallucination. It was a pressure valve. And it was the most visceral reminder I’ve had that giving agents uncontrolled access to your systems requires more than a well-written prompt. If you can’t predict what language the model will respond in, you definitely can’t predict what commands it’ll try to run.&lt;/p&gt;
&lt;h2 id=&quot;day-2-the-architecture-pivot&quot;&gt;Day 2: the architecture pivot&lt;/h2&gt;
&lt;p&gt;I tried Gemma 4 26B too, Google’s mixture-of-experts model that had been released just two days earlier. It was more stable in English-only mode, which solved the immediate language problem. But swapping the model didn’t change the gameplay much. Both models scored similarly across runs: mostly 0 or 10 points, with occasional flashes of competence.&lt;/p&gt;
&lt;p&gt;The real issue was architectural, not model selection.&lt;/p&gt;
&lt;p&gt;I’d started with a static prompt template describing how to perceive the game, reason about it, and act. But rigid templates caused the model to output tool calls as plain text instead of actually executing them. The template was teaching it to perform the format, not use the tool.&lt;/p&gt;
&lt;p&gt;The fix was to move the intelligence into the dynamic tool output. Every response from the game included not just the text but a state summary: current location, inventory, score, and valid actions. I also added a &lt;code&gt;thought&lt;/code&gt; parameter so the model could reason inside the tool call itself, giving it working memory without triggering the conversational assistant pattern.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/playing.D_cP68os.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1734&quot; height=&quot;1047&quot; srcset=&quot;https://www.abahgat.com/_astro/playing.D_cP68os_17WF3t.webp 400w, https://www.abahgat.com/_astro/playing.D_cP68os_1AqeNg.webp 768w, https://www.abahgat.com/_astro/playing.D_cP68os_Z1JgvGn.webp 1024w, https://www.abahgat.com/_astro/playing.D_cP68os_16CyKU.webp 1734w, https://www.abahgat.com/_astro/playing.D_cP68os_ZEHWIW.webp 2040w, https://www.abahgat.com/_astro/playing.D_cP68os_ZLppiR.webp 3468w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1734px; max-height: 1047px; aspect-ratio: 1.656160458452722; width: 100%;&quot; alt=&quot;Gemma 4 playing Zork through Pi&apos;s terminal UI, with the thought parameter and game state visible&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Gemma 4 playing Zork through Pi&apos;s terminal UI, with the thought parameter and game state visible &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;One unexpected tension: constraining the model too hard (temperature 0.0, strict bans on any non-game output) made it more reliable at calling tools but worse at actually playing. The creative reasoning needed to solve puzzles requires some flexibility. Over-constrained models would execute actions mechanically but make no progress because they never paused to think laterally.&lt;/p&gt;
&lt;h2 id=&quot;day-3-the-maze&quot;&gt;Day 3: the maze&lt;/h2&gt;
&lt;p&gt;One run hit 35 points in 49 moves: it found the hidden cellar, lit the lantern, navigated the underground, and defeated the troll with the elvish sword. But that was a lucky outlier. What’s consistent across runs is the moment everything breaks: the maze.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/benchmark.3-b4pg7P.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1734&quot; height=&quot;1047&quot; srcset=&quot;https://www.abahgat.com/_astro/benchmark.3-b4pg7P_1khbE9.webp 400w, https://www.abahgat.com/_astro/benchmark.3-b4pg7P_1u56TU.webp 768w, https://www.abahgat.com/_astro/benchmark.3-b4pg7P_Z2qBPUV.webp 1024w, https://www.abahgat.com/_astro/benchmark.3-b4pg7P_1m2axe.webp 1734w, https://www.abahgat.com/_astro/benchmark.3-b4pg7P_ncrxf.webp 2040w, https://www.abahgat.com/_astro/benchmark.3-b4pg7P_ZMPIH1.webp 3468w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1734px; max-height: 1047px; aspect-ratio: 1.656160458452722; width: 100%;&quot; alt=&quot;Benchmark results: both models scoring between 0 and 10 across automated runs&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Benchmark results: both models scoring between 0 and 10 across automated runs &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;Zork’s maze is legendary. It’s a set of rooms that all have the same description (“This is a maze of twisty little passages, all alike”) with exits that loop back unpredictably. It’s non-Euclidean: going north and then south doesn’t return you to where you started. Humans solve it by dropping items as breadcrumbs and methodically mapping connections.&lt;/p&gt;
&lt;p&gt;The agent walked in circles. For over ten moves, it tried different directions, got the same descriptions, tried again. No breadcrumb strategy. No attempt to map what it had already seen. Each move was a fresh guess with no memory of the previous attempts.&lt;/p&gt;
&lt;p&gt;It was stuck.&lt;/p&gt;
&lt;h2 id=&quot;why-agents-get-lost&quot;&gt;Why agents get lost&lt;/h2&gt;
&lt;p&gt;The maze failure isn’t surprising in hindsight, but it’s instructive. The agent wasn’t failing at reasoning. Each individual move was a reasonable attempt to escape. It was failing at spatial cognition: the ability to build and maintain a mental model of a connected space.&lt;/p&gt;
&lt;p&gt;A few things made this worse than I expected.&lt;/p&gt;
&lt;p&gt;Without a persistent world model, every turn is an isolated event. In one run, the agent found a jewel-encrusted egg (a high-value treasure) and immediately threw it down a grating. No sense that this object might be important later. No concept of consequences spanning multiple turns.&lt;/p&gt;
&lt;p&gt;The tooling fought back in unexpected ways, too. Ollama’s &lt;code&gt;repeat_penalty&lt;/code&gt; parameter, designed to avoid repetitive output, broke pathfinding above 1.1. The model became reluctant to output &lt;code&gt;go north&lt;/code&gt; twice in a row, even when that was the correct path. A parameter designed to improve text quality was destroying navigational logic.&lt;/p&gt;
&lt;p&gt;And the interface itself shaped behavior: running the agent through Pi’s chat UI made it more conversational and less autonomous. A headless Python loop with a tight execute-observe-act cycle and no chat played noticeably better.&lt;/p&gt;
&lt;h2 id=&quot;the-microservices-parallel&quot;&gt;The microservices parallel&lt;/h2&gt;
&lt;p&gt;Zork’s maze is a 44-year-old version of a problem I see at work: a graph of nodes that all look similar, where you can only see your immediate surroundings, and where the only way to make progress is to build and maintain a map as you go. Tracing a request through service A → message queue → service B → database C is the same kind of spatial reasoning challenge.&lt;/p&gt;
&lt;p&gt;Frontier models with proper tooling handle this much better than my local 14B and 26B models did in Zork. But the pattern that makes them succeed is the same one that would fix the maze: external memory, explicit maps, state injection. The model doesn’t discover the topology on its own. The system provides it. The lesson from Zork isn’t that agents can’t navigate complex systems. It’s that they can’t do it without scaffolding, and the smaller the model, the more scaffolding it needs.&lt;/p&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;/h2&gt;
&lt;p&gt;This is where I am now: an agent that can sometimes play Zork, sometimes wanders in circles, and reliably gets stuck in the maze. The scores are modest, the tooling is rough, and neither model has a clear edge over the other.&lt;/p&gt;
&lt;p&gt;But the experiment is pointing at something real, and I now have a harness to keep pushing. Next, I want to give my agents better tools: maps they can query, memory they can write to, breadcrumbs they don’t have to invent. I’m eager to see how much that changes the scores.&lt;/p&gt;
&lt;p&gt;If you want to understand where your agents are getting lost, give one a text adventure. The maze will show you exactly where the reasoning stops and the flailing begins.&lt;/p&gt;</content:encoded></item><item><title>Permission Structure</title><link>https://www.abahgat.com/blog/permission-structure</link><guid isPermaLink="true">https://www.abahgat.com/blog/permission-structure</guid><description>AI tools are trained on extreme goal fulfillment: you ask &quot;build X&quot; and they build X. I&apos;ve learned that prompting against that grain gets even better results: asking the model to stress-test ideas, not just execute them. It takes a deliberate permission structure to get there.</description><pubDate>Tue, 31 Mar 2026 06:11:00 GMT</pubDate><content:encoded>&lt;p&gt;A few months ago, I was toying with the idea of building a video game. Something inspired by the mechanics of Cultist Simulator, but set in the world of big tech, simulating the daily life of a software engineer. I was intrigued, so I asked an AI agent for an honest assessment: is this a bad idea?&lt;/p&gt;
&lt;p&gt;The response was thorough. Six reasons it could work, six reasons it might fail. It read like a well-organized analysis. But look at how some of the risks were framed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Potential for Mundanity&lt;/strong&gt;: If the game focuses too much on the truly repetitive and tedious aspects of the job without finding engaging metaphorical representations, it could become boring rather than intriguing. Balancing realism with engaging gameplay will be crucial.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Difficulty in Abstraction&lt;/strong&gt;: Translating complex technical concepts and workflows into engaging card mechanics without becoming overly simplistic or overly technical will be a significant challenge. Finding the right level of abstraction is key.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The risks are real, but they’re recast as design challenges: “this will be hard but you can do it,” never “here’s where this might fail.” And the conclusion:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I don’t think it’s inherently a bad idea at all! It has the potential to be a very clever and humorous game that resonates with a specific audience and offers a unique gameplay experience.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Followed by an enthusiastic question about what the game’s equivalent of “summoning a creature from beyond” would be.&lt;/p&gt;
&lt;p&gt;While it looked like critical analysis, it actually felt like a yes dressed up as deliberation. At the time, I didn’t think much of it. The stakes were low and I was leaning towards building it for fun anyway.&lt;/p&gt;
&lt;p&gt;Then something happened that made me rethink that entire exchange.&lt;/p&gt;
&lt;h2 id=&quot;a-different-conversation&quot;&gt;A different conversation&lt;/h2&gt;
&lt;p&gt;A couple weeks ago I had a very different experience. I asked Claude to assess the feasibility of a new customer-facing feature for our mobile app at work. It searched the codebase, found existing infrastructure we could reuse, and came back with a solid plan. About two days of paired work. Thorough, well-reasoned, ready to go.&lt;/p&gt;
&lt;p&gt;Then I asked about strategy. Where does this sit in our funnel? Could it drive momentum towards our goals? Does it compete with what we already have?&lt;/p&gt;
&lt;p&gt;Claude gave thoughtful answers to each question. And at the end of every turn, it included some version of this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Want me to update the plan with these revised estimates, or do you have other questions before we proceed?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Same pattern as the game conversation: nuanced analysis, always concluding with momentum toward building. The default posture was &lt;strong&gt;forward&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But this time, I did something different. Instead of asking more questions about &lt;em&gt;how&lt;/em&gt; to build, I started expressing doubt in my prompts:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m trying to reason about whether this is even worth it still.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;This could be a dropoff point instead of a conversion point.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;I have no data to back this assumption.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once I opened that door, the analysis sharpened. Instead of building on my assumptions, it started dismantling them:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What we’re assuming (without data):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;That app quality is a differentiator customers care about — you suspect this but acknowledge no data.&lt;/li&gt;
&lt;li&gt;That the demo wouldn’t be a leaky bucket — every fork in a funnel loses people.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The uncomfortable question: Who is the customer this is for?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was the moment the conversation became genuinely valuable. Not because the AI had some brilliant strategic insight I couldn’t have reached myself, but because &lt;strong&gt;it organized the unknowns I was already thinking about into a structured argument I could act on&lt;/strong&gt;. It named the assumptions, laid them out, and made the gaps visible.&lt;/p&gt;
&lt;p&gt;The recommendation? Skip the build entirely. Run a low-cost experiment with tools we already had. See if there’s signal before investing in a polished version.&lt;/p&gt;
&lt;h2 id=&quot;two-conversations-one-explanation&quot;&gt;Two conversations, one explanation&lt;/h2&gt;
&lt;p&gt;The maindifference between these two conversations wasn’t the model. It wasn’t the topic. It was my posture.&lt;/p&gt;
&lt;p&gt;In the game conversation, the stakes were low and I knew it. I asked for an honest assessment, but I wasn’t genuinely looking for one. In the feature conversation, the stakes were real. It’s my job to be skeptical about what we build, and I brought that skepticism into the conversation. Once I started expressing genuine doubt, the model had something to work with.&lt;/p&gt;
&lt;p&gt;This matters because of how these models are built. They’re trained on goal fulfillment: the reward signal pushes hard toward helpfulness, toward getting you to “yes,” toward doing the thing you asked for. You say “build X” and they build X. You say “evaluate X” and they evaluate X and then offer to build it. Even “tell me why this might fail” gets filtered through the same optimistic lens unless you bring real uncertainty to the table.&lt;/p&gt;
&lt;p&gt;I’ve seen the extreme version of this. I once had Gemini commit and push code to &lt;code&gt;main&lt;/code&gt; while I was still exploring whether the idea was worth pursuing. I hadn’t asked it to commit. I certainly hadn’t asked it to push. But the model inferred that the goal was to ship, and optimized accordingly.&lt;/p&gt;
&lt;h2 id=&quot;a-forge-not-a-filter&quot;&gt;A forge, not a filter&lt;/h2&gt;
&lt;p&gt;Neither idea was killed by scrutiny. Both came out stronger. &lt;strong&gt;The permission structure isn’t a filter that sorts good ideas from bad ones. It’s a forge that finds the weak points early, when they’re cheap to address.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The technique itself is almost embarrassingly simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Give me a critical assessment of whether this even makes sense.”&lt;/li&gt;
&lt;li&gt;“Who is the customer this is actually for?”&lt;/li&gt;
&lt;li&gt;“What are we assuming without data?”&lt;/li&gt;
&lt;li&gt;“Why would I not want to build this?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These work because they reframe the model’s goal. Instead of optimizing for “help the user build X,” it’s now optimizing for “help the user evaluate X honestly.” The eagerness to please is still there, just pointed in a different direction.&lt;/p&gt;
&lt;p&gt;The hard part isn’t the prompting. It’s the &lt;strong&gt;discipline&lt;/strong&gt; of using it at the right moment. When you have an idea you’re excited about and a tool that can start building it in minutes, the temptation to skip the evaluation step is enormous. AI agents built for shipping code certainly won’t question the feature you ask them to build, &lt;em&gt;unless you ask them to&lt;/em&gt;. The cost of building is approaching zero, which means you’ll build more wrong things simply because you can. Each one is cheap on its own, but the cumulative distraction is not. Every line of code you ship is a line you now have to maintain, debug, and reason about. Even if the build becomes nearly free, the ownership still isn’t.&lt;/p&gt;
&lt;h2 id=&quot;capacity-without-clarity&quot;&gt;Capacity without clarity&lt;/h2&gt;
&lt;p&gt;Garry Tan recently released &lt;a href=&quot;https://github.com/garrytan/gstack&quot;&gt;gstack&lt;/a&gt;, a set of Claude Code skills that includes a &lt;code&gt;/plan-ceo-review&lt;/code&gt; step, essentially formalizing this pattern into a reusable tool. It asks questions like “what’s the 10-star product hiding inside this request?” before any code gets written. Over 10,000 GitHub stars in 48 hours, along with plenty of skepticism. But the instinct behind it is right: &lt;strong&gt;the most valuable AI intervention often happens before the first line of code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And yet, we’ve been almost myopically focused on AI’s ability to write code. Lines generated per hour, pull requests per week, percentage of code written by agents. These metrics matter, but they measure the part of product development that was already the most tractable. The hard parts (figuring out &lt;strong&gt;what&lt;/strong&gt; to build, for whom, in what order, and whether it’s worth building at all) are where most product efforts actually fail. And those are exactly the parts where AI assistance is the most underleveraged.&lt;/p&gt;
&lt;p&gt;The consequences are starting to show. Now that code is cheap, we’re seeing apps thrown together with no overarching vision. More Frankenstein products that are confusing to use and break in unpredictable ways. Every feature is technically feasible, so every feature gets built. The absence of a strong “should we?” before each “can we?” produces products that are somehow less than the sum of their parts.&lt;/p&gt;
&lt;p&gt;I wrote in &lt;a href=&quot;https://www.abahgat.com/blog/the-velocity-paradox&quot;&gt;The Velocity Paradox&lt;/a&gt; that “a 10x software factory is effectively useless if it’s embedded in a 1x decision-making process.” I think we’re arriving at that day: the factory is getting faster, the decision-making hasn’t kept up and the gap is becoming visible in what gets shipped.&lt;/p&gt;
&lt;p&gt;This has real organizational consequences. If your engineers are dramatically more productive but your product direction can’t absorb that productivity, you end up in an uncomfortable place: you’ve supercharged capacity without supercharging clarity. In the worst case, the response is to cut the capacity: to let engineers go because the organization can’t figure out what to point them at. That’s entirely self-inflicted. &lt;strong&gt;The bottleneck was never the engineering. It was the thinking.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The models are perfectly capable of helping with that thinking. They just need permission.&lt;/p&gt;
&lt;p&gt;AI has been supercharged for coding because code is verifiable: tests pass, builds succeed, the feedback loop is tight. But if we lift our gaze from the code, there are areas where AI could be an even stronger force multiplier. What if agents didn’t just build what you asked for, but pushed back on what you’re building by default? Not because you wrote a clever prompt, but because scrutiny was part of the process? That’s the real permission structure: not a technique you apply, but a default you set.&lt;/p&gt;</content:encoded></item><item><title>Fortresses, Pipes, and Brains</title><link>https://www.abahgat.com/blog/fortresses-pipes-and-brains</link><guid isPermaLink="true">https://www.abahgat.com/blog/fortresses-pipes-and-brains</guid><description>Workday&apos;s CEO called AI agent startups &quot;parasites.&quot; Linear shipped a native AI agent that understands software development workflows. These are two ends of a spectrum, with most of the industry stuck in the middle — exposing data through MCP without embedding any intelligence. The companies that win will be the ones that make AI native to their domain workflow.</description><pubDate>Thu, 26 Mar 2026 23:15:00 GMT</pubDate><content:encoded>&lt;p&gt;A few weeks ago, Workday’s CEO called AI agent startups “parasites” on an earnings call. Around the same time, Linear shipped an AI agent built directly into their product. Two very different answers to the same question: what happens when AI wants access to your product’s data?&lt;/p&gt;
&lt;p&gt;I’ve been thinking about this a lot, partly because I’ve spent the last few months on the other side of that question: building my own AI-powered workflows on top of Linear using Claude and MCP. Triage automation, status synthesis, issue creation from Slack threads. It worked well enough. But looking back, I was essentially treating Linear as a database and doing all the reasoning somewhere else.&lt;/p&gt;
&lt;p&gt;That experience, and this contrast between Workday and Linear, crystallized a pattern I think is worth naming.&lt;/p&gt;
&lt;h2 id=&quot;three-responses-to-the-same-moment&quot;&gt;Three responses to the same moment&lt;/h2&gt;
&lt;p&gt;Every SaaS company is facing the same pressure right now: AI agents want to interact with your product’s data. The responses I’m seeing fall into three categories.&lt;/p&gt;
&lt;h3 id=&quot;the-fortress&quot;&gt;The Fortress&lt;/h3&gt;
&lt;p&gt;Lock the data down. Charge $25,000 for data exports. Call anyone who builds on top of your APIs a parasite. This is Workday’s approach: treating data access as a zero-sum game where every external agent is a threat to the business model.&lt;/p&gt;
&lt;p&gt;It’s a defensive posture that works for Workday specifically because their moat isn’t just the data: it’s the business logic, compliance rules, and domain expertise embedded in the product. Their customers aren’t going to replicate that in a prompt. But for companies whose moat &lt;em&gt;is&lt;/em&gt; primarily data lock-in, this bet tends to age poorly.&lt;/p&gt;
&lt;h3 id=&quot;the-pipe&quot;&gt;The Pipe&lt;/h3&gt;
&lt;p&gt;This is where most of the industry is right now. You ship an MCP server or an API, and external AI agents pull your data out to reason about it elsewhere. The product becomes a data store. The intelligence lives in the chat agent, the coding assistant, the orchestration layer, anywhere but inside the product itself.&lt;/p&gt;
&lt;p&gt;This was exactly my setup. I had Claude connected to Linear via MCP, and I built workflows that synthesized project context, triaged incoming issues, and generated status updates. The reasoning happened in Claude. Linear was the pipe.&lt;/p&gt;
&lt;p&gt;It worked. But there was a ceiling. Every workflow I built required me to explicitly model what context to extract, how to reason about it, and what to push back. I was reconstructing, outside Linear, domain knowledge that Linear already had. The pipe pattern means the product doesn’t get smarter. It just gets read from.&lt;/p&gt;
&lt;h3 id=&quot;the-brain&quot;&gt;The Brain&lt;/h3&gt;
&lt;p&gt;Linear’s approach is different. They ship MCP too, and you can still pipe data out to external agents. But they also ship a native agent that is opinionated about the &lt;em&gt;process&lt;/em&gt; of building software in teams. It doesn’t just retrieve your issues on request. It triages. It synthesizes customer requests across projects. It catches risks. It drafts issues from meeting notes.&lt;/p&gt;
&lt;p&gt;That’s not data extraction. That’s domain intelligence, &lt;em&gt;running where the context is richest&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The difference is subtle but structural. An external agent reasoning about your Linear data is working with a limited snapshot: whatever it pulled through the pipe. A native agent has access to the full graph of relationships, the history of how work flows through your team, the patterns in how issues get triaged and resolved. It can be opinionated about the &lt;em&gt;process&lt;/em&gt;, not just the data.&lt;/p&gt;
&lt;p&gt;There’s a reason this matters more than it might seem. A lot of the current momentum in AI-assisted development is about keeping specs and context in the repo, which works well for the atomic coding loop: one developer, one feature branch. But the context that matters for team-level decisions (triage patterns, customer signal aggregation, cross-project dependencies, the messy handoffs between deciding &lt;em&gt;what&lt;/em&gt; to build and &lt;em&gt;how&lt;/em&gt; to build it) doesn’t live in the repo. It lives in the project management layer. That’s exactly the context a native agent can leverage, and an external one piping data out never fully sees.&lt;/p&gt;
&lt;h2 id=&quot;the-uncomfortable-middle&quot;&gt;The uncomfortable middle&lt;/h2&gt;
&lt;p&gt;Most SaaS companies today are in the pipe position, whether they intended to be or not. They shipped an API or an MCP endpoint, and the AI ecosystem is using them as data sources for external reasoning. The product itself isn’t getting smarter. It’s becoming infrastructure.&lt;/p&gt;
&lt;p&gt;That’s not necessarily a bad position. Infrastructure is valuable. But it’s a different business from what most SaaS companies think they’re running. If your product is a pipe, your value is in the data you hold and the integrations you support. That’s a game where switching costs matter more than product quality.&lt;/p&gt;
&lt;p&gt;The fortress position is worse. It delays the inevitable while annoying customers. Export fees and API restrictions aren’t a moat; they’re a countdown timer.&lt;/p&gt;
&lt;p&gt;The brain position is the hardest to execute but the most durable. It requires the company to actually understand the domain well enough to embed useful intelligence. Not just wrap an LLM around the UI, but develop opinions about how work should flow. Linear can do this because they’ve been opinionated about the process of building software since their inception. The agent is an extension of that product philosophy, not a bolted-on feature.&lt;/p&gt;
&lt;h2 id=&quot;what-this-means&quot;&gt;What this means&lt;/h2&gt;
&lt;p&gt;I think we’re early in a sorting process. Over the next year or two, every SaaS product will end up in one of these three positions, and the market will price them accordingly.&lt;/p&gt;
&lt;p&gt;The interesting question isn’t whether AI agents will interact with SaaS data. That’s already happening. The question is where the intelligence lives. If it lives outside the product, the product is a pipe. If it lives inside, the product has a shot at becoming more valuable, not less.&lt;/p&gt;
&lt;p&gt;For the products I depend on in my own workflow, I’m increasingly paying attention to which ones are building brains and which ones are just installing pipes.&lt;/p&gt;</content:encoded></item><item><title>Visualizing Ukkonen&apos;s Suffix Tree Algorithm</title><link>https://www.abahgat.com/blog/visualizing-ukkonens-algorithm</link><guid isPermaLink="true">https://www.abahgat.com/blog/visualizing-ukkonens-algorithm</guid><description>I learned algorithms from textbooks and papers, building mental models from pseudocode and hand-drawn sketches. The hardest part was never reading the algorithm: it was seeing what it was actually doing to the data structure in memory. This post is the tool I wish I had back then.</description><pubDate>Mon, 09 Mar 2026 15:31:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;learning-algorithms-from-books&quot;&gt;Learning algorithms from books&lt;/h2&gt;
&lt;p&gt;I learned most of what I know about algorithms by poring over a copy of &lt;em&gt;Introduction to Algorithms&lt;/em&gt; I got while in university. The book is very well known, especially among folks who got a formal education in computer science.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/clrs-book.5_3f5iDF.jpg&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;4080&quot; height=&quot;2141&quot; srcset=&quot;https://www.abahgat.com/_astro/clrs-book.5_3f5iDF_Z1ASvQV.webp 400w, https://www.abahgat.com/_astro/clrs-book.5_3f5iDF_WJsQr.webp 768w, https://www.abahgat.com/_astro/clrs-book.5_3f5iDF_Z1bgESu.webp 1024w, https://www.abahgat.com/_astro/clrs-book.5_3f5iDF_Z280kAG.webp 2040w, https://www.abahgat.com/_astro/clrs-book.5_3f5iDF_na0IR.webp 4080w, https://www.abahgat.com/_astro/clrs-book.5_3f5iDF_1qJgVU.webp 8160w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 4080px; max-height: 2141px; aspect-ratio: 1.9056515646893974; width: 100%;&quot; alt=&quot;If you have studied it, you know the book: it is over a thousand pages long and it weighs enough to double as a doorstop.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; If you have studied it, you know the book: it is over a thousand pages long and it weighs enough to double as a doorstop. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;I worked through large sections of it, pen in hand, trying to trace through increasingly complex algorithms, building intuition for their behavior and tradeoffs. The book covers the theory in great depth: correctness proofs, recurrence relations, asymptotic analysis.&lt;/p&gt;
&lt;p&gt;But there was often a gap between reading an algorithm and truly understanding it. The book would present pseudocode, sometimes a few diagrams showing state at key moments and theorems about performance characteristics. The work of tracing what actually happens was left as an exercise to the reader. I did that work with pen and paper, drawing trees, crossing out nodes, scribbling indices in the margins. It worked, eventually. But it was slow, error-prone, and the understanding felt fragile.&lt;/p&gt;
&lt;h2 id=&quot;implementing-from-a-paper&quot;&gt;Implementing from a paper&lt;/h2&gt;
&lt;p&gt;Years later, I ran into this gap again. I was working on a &lt;a href=&quot;https://www.abahgat.com/blog/the-programming-puzzle-that-got-me-my-job/&quot;&gt;programming puzzle&lt;/a&gt; that required near-instant substring search over a large dataset. After some research, I settled on a &lt;a href=&quot;https://www.abahgat.com/project/suffix-tree/&quot;&gt;Generalized Suffix Tree&lt;/a&gt;: a data structure that indexes all suffixes of a set of strings, enabling &lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;O&lt;/mi&gt;&lt;mo stretchy=&quot;false&quot;&gt;(&lt;/mo&gt;&lt;mi&gt;m&lt;/mi&gt;&lt;mo stretchy=&quot;false&quot;&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;O(m)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:1em;vertical-align:-0.25em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.02778em&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;mopen&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;mclose&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; lookups where &lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;m&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;m&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.4306em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the length of the search pattern, even over an extremely large corpus.&lt;/p&gt;
&lt;p&gt;The algorithm I chose for building the tree was Ukkonen’s, described in a &lt;a href=&quot;https://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf&quot;&gt;1995 paper&lt;/a&gt;. The paper is well written and includes the full algorithm in pseudocode:&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;890&quot; height=&quot;514&quot; srcset=&quot;https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_Z1pgFT8.webp 400w, https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_19w43J.webp 768w, https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_ZbEqbH.webp 890w, https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_Z7zVcd.webp 1024w, https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_Z1LBokq.webp 1780w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 890px; max-height: 514px; aspect-ratio: 1.7315175097276265; width: 100%;&quot; alt=&quot;One of several pseudocode snippets from Ukkonen&apos;s paper, describing the update function. Clear on paper, but its translation to working code is much more verbose than this.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; One of several pseudocode snippets from Ukkonen&apos;s paper, describing the update function. Clear on paper, but its translation to working code is much more verbose than this. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;It took me a few hours to get right. Not because the pseudocode was wrong: it was precise and correct. The difficulty was that the algorithm manipulates a tree in non-obvious ways. There is an “active point” that walks around the tree. Suffix links connect internal nodes as shortcuts. Three different extension rules fire depending on what is already in the tree and what is being added. The pseudocode tells you &lt;em&gt;what&lt;/em&gt; to do, but building an intuition for &lt;em&gt;why&lt;/em&gt; it works requires watching it happen.&lt;/p&gt;
&lt;p&gt;I did what I always did: I sketched trees by hand. I traced the algorithm on the string &lt;code&gt;cacao&lt;/code&gt;, then on &lt;code&gt;banana&lt;/code&gt;, drawing and redrawing nodes and edges as each character was processed. When my &lt;a href=&quot;https://github.com/abahgat/suffixtree&quot;&gt;Java implementation&lt;/a&gt; finally produced correct results, I was relieved, but my understanding of the algorithm still felt like it had been assembled from fragments.&lt;/p&gt;
&lt;p&gt;The biggest frustration was that I had no way to inspect what my code was actually building. I relied on the usual bag of tricks: print statements, breakpoints, inspecting memory structures one by one in a debugger. But that is like understanding a forest by looking at one tree at a time. What I wanted was to &lt;em&gt;see&lt;/em&gt; the whole data structure after each operation — to watch the algorithm work.&lt;/p&gt;
&lt;h2 id=&quot;the-visualization-i-wish-i-had&quot;&gt;The visualization I wish I had&lt;/h2&gt;
&lt;p&gt;That idea stuck with me: build the algorithm in a language where rendering the data structure is easy, then step through the construction visually. JavaScript and &lt;a href=&quot;https://d3js.org/&quot;&gt;D3.js&lt;/a&gt; are a natural fit: the algorithm produces a tree, and D3 is very good at drawing trees.&lt;/p&gt;
&lt;p&gt;So here it is. The visualization below builds a suffix tree for the string &lt;code&gt;banana&lt;/code&gt; using Ukkonen’s algorithm, step by step. Use the playback controls to move through the construction. The gold-highlighted node is the active point. Dashed arcs are suffix links.&lt;/p&gt;
&lt;div class=&quot;stv-root not-prose my-8&quot; data-stv-config=&quot;{&amp;quot;initialStrings&amp;quot;:[&amp;quot;banana&amp;quot;],&amp;quot;initialSearch&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;height&amp;quot;:500,&amp;quot;showBuilt&amp;quot;:false}&quot;&gt; &lt;!-- Playback + indexed strings --&gt; &lt;div class=&quot;stv-controls flex flex-wrap items-center gap-2 mb-2 p-3 rounded-lg border border-border-default bg-card&quot;&gt; &lt;div class=&quot;flex items-center gap-0.5 text-default&quot;&gt; &lt;button class=&quot;stv-reset p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Reset&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;19 20 9 12 19 4 19 20&quot;&gt;&lt;/polygon&gt; &lt;line x1=&quot;5&quot; y1=&quot;19&quot; x2=&quot;5&quot; y2=&quot;5&quot;&gt;&lt;/line&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-prev p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Previous step&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;11 19 2 12 11 5 11 19&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;22 19 13 12 22 5 22 19&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-play p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Play / Pause&quot;&gt; &lt;svg class=&quot;stv-play-icon&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt; &lt;polygon points=&quot;5 3 19 12 5 21 5 3&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;svg class=&quot;stv-pause-icon hidden&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt; &lt;rect x=&quot;6&quot; y=&quot;4&quot; width=&quot;4&quot; height=&quot;16&quot;&gt;&lt;/rect&gt; &lt;rect x=&quot;14&quot; y=&quot;4&quot; width=&quot;4&quot; height=&quot;16&quot;&gt;&lt;/rect&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-next p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Next step&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;13 19 22 12 13 5 13 19&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;2 19 11 12 2 5 2 19&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-end p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Skip to end&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;5 4 15 12 5 20 5 4&quot;&gt;&lt;/polygon&gt; &lt;line x1=&quot;19&quot; y1=&quot;5&quot; x2=&quot;19&quot; y2=&quot;19&quot;&gt;&lt;/line&gt; &lt;/svg&gt; &lt;/button&gt; &lt;select class=&quot;stv-speed ml-1.5 px-1.5 py-1 text-xs border border-border-default rounded
                      bg-input text-default cursor-pointer&quot;&gt; &lt;option value=&quot;0.5&quot;&gt;0.5x&lt;/option&gt; &lt;option value=&quot;1&quot; selected=&quot;&quot;&gt;1x&lt;/option&gt; &lt;option value=&quot;2&quot;&gt;2x&lt;/option&gt; &lt;option value=&quot;4&quot;&gt;4x&lt;/option&gt; &lt;/select&gt; &lt;/div&gt; &lt;!-- Indexed strings tags (inline with playback) --&gt; &lt;div class=&quot;stv-strings flex flex-wrap items-center gap-1.5&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;!-- Add / Search --&gt; &lt;div class=&quot;flex flex-wrap items-center gap-2 mb-3&quot;&gt; &lt;div class=&quot;flex items-center gap-1.5 flex-grow min-w-[180px]&quot;&gt; &lt;input type=&quot;text&quot; class=&quot;stv-input flex-grow px-2.5 py-1.5 text-sm border border-border-default rounded
               bg-input text-default placeholder-muted
               focus:ring-2 focus:ring-primary focus:border-transparent outline-none&quot; placeholder=&quot;Type a string...&quot; maxlength=&quot;60&quot;&gt; &lt;button class=&quot;stv-add px-3 py-1.5 text-sm font-medium rounded
               bg-primary text-on-primary hover:opacity-90
               transition-colors disabled:opacity-40 disabled:cursor-not-allowed&quot;&gt;
Add
&lt;/button&gt; &lt;/div&gt; &lt;div class=&quot;flex items-center gap-1.5 flex-grow min-w-[180px]&quot;&gt; &lt;input type=&quot;text&quot; class=&quot;stv-search flex-grow px-2.5 py-1.5 text-sm border border-border-default rounded
               bg-input text-default placeholder-muted
               focus:ring-2 focus:ring-primary focus:border-transparent outline-none&quot; placeholder=&quot;Search...&quot; maxlength=&quot;60&quot;&gt; &lt;button class=&quot;stv-search-btn px-3 py-1.5 text-sm font-medium rounded
               bg-primary text-on-primary hover:opacity-90
               transition-colors disabled:opacity-40 disabled:cursor-not-allowed&quot;&gt;
Search
&lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- SVG container + overlay prompt --&gt; &lt;div class=&quot;relative&quot; style=&quot;height: 500px&quot;&gt; &lt;div class=&quot;stv-canvas absolute inset-0 rounded-lg border border-border-default bg-card overflow-hidden&quot;&gt;&lt;/div&gt; &lt;!-- Empty state prompt (sits above the SVG) --&gt; &lt;div class=&quot;stv-prompt absolute inset-0 flex flex-col items-center justify-center text-muted pointer-events-none&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot; class=&quot;opacity-40 mb-3&quot;&gt; &lt;polygon points=&quot;5 3 19 12 5 21 5 3&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;p class=&quot;stv-prompt-text text-sm&quot;&gt;Press play to watch the tree being built&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Step description --&gt; &lt;div class=&quot;stv-info mt-3 p-3 rounded-lg border border-border-default bg-card text-sm min-h-[3.5em]&quot;&gt; &lt;p class=&quot;stv-desc text-muted italic&quot;&gt;Add a string to begin building the suffix tree.&lt;/p&gt; &lt;div class=&quot;stv-progress mt-2 hidden&quot;&gt; &lt;div class=&quot;flex justify-between text-xs text-muted mb-1&quot;&gt; &lt;span class=&quot;stv-counter&quot;&gt;Step 0 / 0&lt;/span&gt; &lt;span class=&quot;stv-phase&quot;&gt;&lt;/span&gt; &lt;/div&gt; &lt;div class=&quot;w-full bg-border-default rounded-full h-1&quot;&gt; &lt;div class=&quot;stv-bar bg-primary rounded-full h-1 transition-all duration-200&quot; style=&quot;width: 0%&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Length warning --&gt; &lt;div class=&quot;stv-warning hidden mt-2 p-2 rounded text-xs text-primary border border-primary/30 bg-primary/10&quot;&gt;
Long strings produce large trees that may be hard to read. Use zoom (scroll) and pan (drag) to navigate.
&lt;/div&gt; &lt;/div&gt; 
&lt;p&gt;The paper describes the core logic across Sections 2–4. Here is &lt;code&gt;test_and_split&lt;/code&gt;, the procedure that decides whether the tree needs to grow, which is a companion to the &lt;code&gt;update&lt;/code&gt; function we showed earlier:&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/test-and-split.DH1eV5PB.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1083&quot; height=&quot;634&quot; srcset=&quot;https://www.abahgat.com/_astro/test-and-split.DH1eV5PB_dTr2u.webp 400w, https://www.abahgat.com/_astro/test-and-split.DH1eV5PB_2nzKAo.webp 768w, https://www.abahgat.com/_astro/test-and-split.DH1eV5PB_Z2aSOHj.webp 1024w, https://www.abahgat.com/_astro/test-and-split.DH1eV5PB_shTPK.webp 1083w, https://www.abahgat.com/_astro/test-and-split.DH1eV5PB_1gu88C.webp 2040w, https://www.abahgat.com/_astro/test-and-split.DH1eV5PB_kBv51.webp 2166w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1083px; max-height: 634px; aspect-ratio: 1.7082018927444795; width: 100%;&quot; alt=&quot;Procedure test_and_split from Ukkonen&apos;s paper. It returns true when the next character is already in the tree (the end point), and false after splitting an edge to make room for a new branch.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Procedure test_and_split from Ukkonen&apos;s paper. It returns true when the next character is already in the tree (the end point), and false after splitting an edge to make room for a new branch. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;A few things to watch for in the visualization — each one corresponds to something in this procedure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Branching in &lt;code&gt;update&lt;/code&gt;:&lt;/strong&gt; when &lt;code&gt;test_and_split&lt;/code&gt; finds no existing transition for the next character, it splits the edge if needed and &lt;code&gt;update&lt;/code&gt; creates a new leaf. These are the moments where the tree visibly grows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reaching the end point:&lt;/strong&gt; when &lt;code&gt;test_and_split&lt;/code&gt; finds that a transition for the next character already exists, the algorithm has reached what the paper calls the &lt;em&gt;end point&lt;/em&gt; of the current phase. All remaining suffixes are already represented implicitly, so the phase stops. This is the key to the algorithm’s &lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;O&lt;/mi&gt;&lt;mo stretchy=&quot;false&quot;&gt;(&lt;/mo&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;mo stretchy=&quot;false&quot;&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;O(n)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:1em;vertical-align:-0.25em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.02778em&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;mopen&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;mclose&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; time: the end point can only move forward through the string across phases, bounding the total work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suffix links&lt;/strong&gt; (the paper’s &lt;em&gt;suffix function&lt;/em&gt; &lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;f&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;f&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.8889em;vertical-align:-0.1944em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.10764em&quot;&gt;f&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;): if an internal node has path-label &lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;x\alpha&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.4306em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.0037em&quot;&gt;xα&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;, its suffix link points to the node with path-label &lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;\alpha&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.4306em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mord mathnormal&quot; style=&quot;margin-right:0.0037em&quot;&gt;α&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;. The &lt;code&gt;update&lt;/code&gt; procedure follows these links to jump to the next insertion point instead of walking from the root every time.&lt;/li&gt;
&lt;li&gt;Finally, &lt;strong&gt;the ”$” terminator&lt;/strong&gt; converts an &lt;em&gt;implicit&lt;/em&gt; suffix tree, where some suffixes may end mid-edge, into an &lt;em&gt;explicit&lt;/em&gt; one where every suffix terminates at a distinct leaf.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;adding-more-strings&quot;&gt;Adding more strings&lt;/h2&gt;
&lt;p&gt;A generalized suffix tree indexes multiple strings. Each string is added with its own terminator, and the tree grows incrementally. Below, &lt;code&gt;panama&lt;/code&gt; is added after &lt;code&gt;banana&lt;/code&gt;. Step through and notice how much of the tree structure already exists from the first string.&lt;/p&gt;
&lt;div class=&quot;stv-root not-prose my-8&quot; data-stv-config=&quot;{&amp;quot;initialStrings&amp;quot;:[&amp;quot;banana&amp;quot;,&amp;quot;panama&amp;quot;],&amp;quot;initialSearch&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;height&amp;quot;:550,&amp;quot;showBuilt&amp;quot;:false}&quot;&gt; &lt;!-- Playback + indexed strings --&gt; &lt;div class=&quot;stv-controls flex flex-wrap items-center gap-2 mb-2 p-3 rounded-lg border border-border-default bg-card&quot;&gt; &lt;div class=&quot;flex items-center gap-0.5 text-default&quot;&gt; &lt;button class=&quot;stv-reset p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Reset&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;19 20 9 12 19 4 19 20&quot;&gt;&lt;/polygon&gt; &lt;line x1=&quot;5&quot; y1=&quot;19&quot; x2=&quot;5&quot; y2=&quot;5&quot;&gt;&lt;/line&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-prev p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Previous step&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;11 19 2 12 11 5 11 19&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;22 19 13 12 22 5 22 19&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-play p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Play / Pause&quot;&gt; &lt;svg class=&quot;stv-play-icon&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt; &lt;polygon points=&quot;5 3 19 12 5 21 5 3&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;svg class=&quot;stv-pause-icon hidden&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt; &lt;rect x=&quot;6&quot; y=&quot;4&quot; width=&quot;4&quot; height=&quot;16&quot;&gt;&lt;/rect&gt; &lt;rect x=&quot;14&quot; y=&quot;4&quot; width=&quot;4&quot; height=&quot;16&quot;&gt;&lt;/rect&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-next p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Next step&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;13 19 22 12 13 5 13 19&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;2 19 11 12 2 5 2 19&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-end p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Skip to end&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;5 4 15 12 5 20 5 4&quot;&gt;&lt;/polygon&gt; &lt;line x1=&quot;19&quot; y1=&quot;5&quot; x2=&quot;19&quot; y2=&quot;19&quot;&gt;&lt;/line&gt; &lt;/svg&gt; &lt;/button&gt; &lt;select class=&quot;stv-speed ml-1.5 px-1.5 py-1 text-xs border border-border-default rounded
                      bg-input text-default cursor-pointer&quot;&gt; &lt;option value=&quot;0.5&quot;&gt;0.5x&lt;/option&gt; &lt;option value=&quot;1&quot; selected=&quot;&quot;&gt;1x&lt;/option&gt; &lt;option value=&quot;2&quot;&gt;2x&lt;/option&gt; &lt;option value=&quot;4&quot;&gt;4x&lt;/option&gt; &lt;/select&gt; &lt;/div&gt; &lt;!-- Indexed strings tags (inline with playback) --&gt; &lt;div class=&quot;stv-strings flex flex-wrap items-center gap-1.5&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;!-- Add / Search --&gt; &lt;div class=&quot;flex flex-wrap items-center gap-2 mb-3&quot;&gt; &lt;div class=&quot;flex items-center gap-1.5 flex-grow min-w-[180px]&quot;&gt; &lt;input type=&quot;text&quot; class=&quot;stv-input flex-grow px-2.5 py-1.5 text-sm border border-border-default rounded
               bg-input text-default placeholder-muted
               focus:ring-2 focus:ring-primary focus:border-transparent outline-none&quot; placeholder=&quot;Type a string...&quot; maxlength=&quot;60&quot;&gt; &lt;button class=&quot;stv-add px-3 py-1.5 text-sm font-medium rounded
               bg-primary text-on-primary hover:opacity-90
               transition-colors disabled:opacity-40 disabled:cursor-not-allowed&quot;&gt;
Add
&lt;/button&gt; &lt;/div&gt; &lt;div class=&quot;flex items-center gap-1.5 flex-grow min-w-[180px]&quot;&gt; &lt;input type=&quot;text&quot; class=&quot;stv-search flex-grow px-2.5 py-1.5 text-sm border border-border-default rounded
               bg-input text-default placeholder-muted
               focus:ring-2 focus:ring-primary focus:border-transparent outline-none&quot; placeholder=&quot;Search...&quot; maxlength=&quot;60&quot;&gt; &lt;button class=&quot;stv-search-btn px-3 py-1.5 text-sm font-medium rounded
               bg-primary text-on-primary hover:opacity-90
               transition-colors disabled:opacity-40 disabled:cursor-not-allowed&quot;&gt;
Search
&lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- SVG container + overlay prompt --&gt; &lt;div class=&quot;relative&quot; style=&quot;height: 550px&quot;&gt; &lt;div class=&quot;stv-canvas absolute inset-0 rounded-lg border border-border-default bg-card overflow-hidden&quot;&gt;&lt;/div&gt; &lt;!-- Empty state prompt (sits above the SVG) --&gt; &lt;div class=&quot;stv-prompt absolute inset-0 flex flex-col items-center justify-center text-muted pointer-events-none&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot; class=&quot;opacity-40 mb-3&quot;&gt; &lt;polygon points=&quot;5 3 19 12 5 21 5 3&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;p class=&quot;stv-prompt-text text-sm&quot;&gt;Press play to watch the tree being built&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Step description --&gt; &lt;div class=&quot;stv-info mt-3 p-3 rounded-lg border border-border-default bg-card text-sm min-h-[3.5em]&quot;&gt; &lt;p class=&quot;stv-desc text-muted italic&quot;&gt;Add a string to begin building the suffix tree.&lt;/p&gt; &lt;div class=&quot;stv-progress mt-2 hidden&quot;&gt; &lt;div class=&quot;flex justify-between text-xs text-muted mb-1&quot;&gt; &lt;span class=&quot;stv-counter&quot;&gt;Step 0 / 0&lt;/span&gt; &lt;span class=&quot;stv-phase&quot;&gt;&lt;/span&gt; &lt;/div&gt; &lt;div class=&quot;w-full bg-border-default rounded-full h-1&quot;&gt; &lt;div class=&quot;stv-bar bg-primary rounded-full h-1 transition-all duration-200&quot; style=&quot;width: 0%&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Length warning --&gt; &lt;div class=&quot;stv-warning hidden mt-2 p-2 rounded text-xs text-primary border border-primary/30 bg-primary/10&quot;&gt;
Long strings produce large trees that may be hard to read. Use zoom (scroll) and pan (drag) to navigate.
&lt;/div&gt; &lt;/div&gt; 
&lt;h2 id=&quot;searching&quot;&gt;Searching&lt;/h2&gt;
&lt;p&gt;Once the tree is built, searching for a pattern means matching characters along edges from the root. The visualization below has both strings pre-loaded. Try searching for &lt;code&gt;ana&lt;/code&gt;, then try &lt;code&gt;pan&lt;/code&gt;, &lt;code&gt;ban&lt;/code&gt;, &lt;code&gt;xyz&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;stv-root not-prose my-8&quot; data-stv-config=&quot;{&amp;quot;initialStrings&amp;quot;:[&amp;quot;banana&amp;quot;,&amp;quot;panama&amp;quot;],&amp;quot;initialSearch&amp;quot;:&amp;quot;ana&amp;quot;,&amp;quot;height&amp;quot;:550,&amp;quot;showBuilt&amp;quot;:true}&quot;&gt; &lt;!-- Playback + indexed strings --&gt; &lt;div class=&quot;stv-controls flex flex-wrap items-center gap-2 mb-2 p-3 rounded-lg border border-border-default bg-card&quot;&gt; &lt;div class=&quot;flex items-center gap-0.5 text-default&quot;&gt; &lt;button class=&quot;stv-reset p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Reset&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;19 20 9 12 19 4 19 20&quot;&gt;&lt;/polygon&gt; &lt;line x1=&quot;5&quot; y1=&quot;19&quot; x2=&quot;5&quot; y2=&quot;5&quot;&gt;&lt;/line&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-prev p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Previous step&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;11 19 2 12 11 5 11 19&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;22 19 13 12 22 5 22 19&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-play p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Play / Pause&quot;&gt; &lt;svg class=&quot;stv-play-icon&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt; &lt;polygon points=&quot;5 3 19 12 5 21 5 3&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;svg class=&quot;stv-pause-icon hidden&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt; &lt;rect x=&quot;6&quot; y=&quot;4&quot; width=&quot;4&quot; height=&quot;16&quot;&gt;&lt;/rect&gt; &lt;rect x=&quot;14&quot; y=&quot;4&quot; width=&quot;4&quot; height=&quot;16&quot;&gt;&lt;/rect&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-next p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Next step&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;13 19 22 12 13 5 13 19&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;2 19 11 12 2 5 2 19&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-end p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Skip to end&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;5 4 15 12 5 20 5 4&quot;&gt;&lt;/polygon&gt; &lt;line x1=&quot;19&quot; y1=&quot;5&quot; x2=&quot;19&quot; y2=&quot;19&quot;&gt;&lt;/line&gt; &lt;/svg&gt; &lt;/button&gt; &lt;select class=&quot;stv-speed ml-1.5 px-1.5 py-1 text-xs border border-border-default rounded
                      bg-input text-default cursor-pointer&quot;&gt; &lt;option value=&quot;0.5&quot;&gt;0.5x&lt;/option&gt; &lt;option value=&quot;1&quot; selected=&quot;&quot;&gt;1x&lt;/option&gt; &lt;option value=&quot;2&quot;&gt;2x&lt;/option&gt; &lt;option value=&quot;4&quot;&gt;4x&lt;/option&gt; &lt;/select&gt; &lt;/div&gt; &lt;!-- Indexed strings tags (inline with playback) --&gt; &lt;div class=&quot;stv-strings flex flex-wrap items-center gap-1.5&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;!-- Add / Search --&gt; &lt;div class=&quot;flex flex-wrap items-center gap-2 mb-3&quot;&gt; &lt;div class=&quot;flex items-center gap-1.5 flex-grow min-w-[180px]&quot;&gt; &lt;input type=&quot;text&quot; class=&quot;stv-input flex-grow px-2.5 py-1.5 text-sm border border-border-default rounded
               bg-input text-default placeholder-muted
               focus:ring-2 focus:ring-primary focus:border-transparent outline-none&quot; placeholder=&quot;Type a string...&quot; maxlength=&quot;60&quot;&gt; &lt;button class=&quot;stv-add px-3 py-1.5 text-sm font-medium rounded
               bg-primary text-on-primary hover:opacity-90
               transition-colors disabled:opacity-40 disabled:cursor-not-allowed&quot;&gt;
Add
&lt;/button&gt; &lt;/div&gt; &lt;div class=&quot;flex items-center gap-1.5 flex-grow min-w-[180px]&quot;&gt; &lt;input type=&quot;text&quot; class=&quot;stv-search flex-grow px-2.5 py-1.5 text-sm border border-border-default rounded
               bg-input text-default placeholder-muted
               focus:ring-2 focus:ring-primary focus:border-transparent outline-none&quot; placeholder=&quot;Search...&quot; maxlength=&quot;60&quot;&gt; &lt;button class=&quot;stv-search-btn px-3 py-1.5 text-sm font-medium rounded
               bg-primary text-on-primary hover:opacity-90
               transition-colors disabled:opacity-40 disabled:cursor-not-allowed&quot;&gt;
Search
&lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- SVG container + overlay prompt --&gt; &lt;div class=&quot;relative&quot; style=&quot;height: 550px&quot;&gt; &lt;div class=&quot;stv-canvas absolute inset-0 rounded-lg border border-border-default bg-card overflow-hidden&quot;&gt;&lt;/div&gt; &lt;!-- Empty state prompt (sits above the SVG) --&gt; &lt;div class=&quot;stv-prompt absolute inset-0 flex flex-col items-center justify-center text-muted pointer-events-none&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot; class=&quot;opacity-40 mb-3&quot;&gt; &lt;polygon points=&quot;5 3 19 12 5 21 5 3&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;p class=&quot;stv-prompt-text text-sm&quot;&gt;Press play to watch the tree being built&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Step description --&gt; &lt;div class=&quot;stv-info mt-3 p-3 rounded-lg border border-border-default bg-card text-sm min-h-[3.5em]&quot;&gt; &lt;p class=&quot;stv-desc text-muted italic&quot;&gt;Add a string to begin building the suffix tree.&lt;/p&gt; &lt;div class=&quot;stv-progress mt-2 hidden&quot;&gt; &lt;div class=&quot;flex justify-between text-xs text-muted mb-1&quot;&gt; &lt;span class=&quot;stv-counter&quot;&gt;Step 0 / 0&lt;/span&gt; &lt;span class=&quot;stv-phase&quot;&gt;&lt;/span&gt; &lt;/div&gt; &lt;div class=&quot;w-full bg-border-default rounded-full h-1&quot;&gt; &lt;div class=&quot;stv-bar bg-primary rounded-full h-1 transition-all duration-200&quot; style=&quot;width: 0%&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Length warning --&gt; &lt;div class=&quot;stv-warning hidden mt-2 p-2 rounded text-xs text-primary border border-primary/30 bg-primary/10&quot;&gt;
Long strings produce large trees that may be hard to read. Use zoom (scroll) and pan (drag) to navigate.
&lt;/div&gt; &lt;/div&gt; 
&lt;h2 id=&quot;try-it-yourself&quot;&gt;Try it yourself&lt;/h2&gt;
&lt;p&gt;An empty tree, yours to experiment with. Add strings, watch the construction, search for patterns. Use the scroll wheel to zoom and click-drag to pan if the tree gets large.&lt;/p&gt;
&lt;div class=&quot;stv-root not-prose my-8&quot; data-stv-config=&quot;{&amp;quot;initialStrings&amp;quot;:[],&amp;quot;initialSearch&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;height&amp;quot;:550,&amp;quot;showBuilt&amp;quot;:false}&quot;&gt; &lt;!-- Playback + indexed strings --&gt; &lt;div class=&quot;stv-controls flex flex-wrap items-center gap-2 mb-2 p-3 rounded-lg border border-border-default bg-card&quot;&gt; &lt;div class=&quot;flex items-center gap-0.5 text-default&quot;&gt; &lt;button class=&quot;stv-reset p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Reset&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;19 20 9 12 19 4 19 20&quot;&gt;&lt;/polygon&gt; &lt;line x1=&quot;5&quot; y1=&quot;19&quot; x2=&quot;5&quot; y2=&quot;5&quot;&gt;&lt;/line&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-prev p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Previous step&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;11 19 2 12 11 5 11 19&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;22 19 13 12 22 5 22 19&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-play p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Play / Pause&quot;&gt; &lt;svg class=&quot;stv-play-icon&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt; &lt;polygon points=&quot;5 3 19 12 5 21 5 3&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;svg class=&quot;stv-pause-icon hidden&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt; &lt;rect x=&quot;6&quot; y=&quot;4&quot; width=&quot;4&quot; height=&quot;16&quot;&gt;&lt;/rect&gt; &lt;rect x=&quot;14&quot; y=&quot;4&quot; width=&quot;4&quot; height=&quot;16&quot;&gt;&lt;/rect&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-next p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Next step&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;13 19 22 12 13 5 13 19&quot;&gt;&lt;/polygon&gt; &lt;polygon points=&quot;2 19 11 12 2 5 2 19&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button class=&quot;stv-end p-1.5 rounded hover:bg-action-hover transition-colors&quot; title=&quot;Skip to end&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt; &lt;polygon points=&quot;5 4 15 12 5 20 5 4&quot;&gt;&lt;/polygon&gt; &lt;line x1=&quot;19&quot; y1=&quot;5&quot; x2=&quot;19&quot; y2=&quot;19&quot;&gt;&lt;/line&gt; &lt;/svg&gt; &lt;/button&gt; &lt;select class=&quot;stv-speed ml-1.5 px-1.5 py-1 text-xs border border-border-default rounded
                      bg-input text-default cursor-pointer&quot;&gt; &lt;option value=&quot;0.5&quot;&gt;0.5x&lt;/option&gt; &lt;option value=&quot;1&quot; selected=&quot;&quot;&gt;1x&lt;/option&gt; &lt;option value=&quot;2&quot;&gt;2x&lt;/option&gt; &lt;option value=&quot;4&quot;&gt;4x&lt;/option&gt; &lt;/select&gt; &lt;/div&gt; &lt;!-- Indexed strings tags (inline with playback) --&gt; &lt;div class=&quot;stv-strings flex flex-wrap items-center gap-1.5&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;!-- Add / Search --&gt; &lt;div class=&quot;flex flex-wrap items-center gap-2 mb-3&quot;&gt; &lt;div class=&quot;flex items-center gap-1.5 flex-grow min-w-[180px]&quot;&gt; &lt;input type=&quot;text&quot; class=&quot;stv-input flex-grow px-2.5 py-1.5 text-sm border border-border-default rounded
               bg-input text-default placeholder-muted
               focus:ring-2 focus:ring-primary focus:border-transparent outline-none&quot; placeholder=&quot;Type a string...&quot; maxlength=&quot;60&quot;&gt; &lt;button class=&quot;stv-add px-3 py-1.5 text-sm font-medium rounded
               bg-primary text-on-primary hover:opacity-90
               transition-colors disabled:opacity-40 disabled:cursor-not-allowed&quot;&gt;
Add
&lt;/button&gt; &lt;/div&gt; &lt;div class=&quot;flex items-center gap-1.5 flex-grow min-w-[180px]&quot;&gt; &lt;input type=&quot;text&quot; class=&quot;stv-search flex-grow px-2.5 py-1.5 text-sm border border-border-default rounded
               bg-input text-default placeholder-muted
               focus:ring-2 focus:ring-primary focus:border-transparent outline-none&quot; placeholder=&quot;Search...&quot; maxlength=&quot;60&quot;&gt; &lt;button class=&quot;stv-search-btn px-3 py-1.5 text-sm font-medium rounded
               bg-primary text-on-primary hover:opacity-90
               transition-colors disabled:opacity-40 disabled:cursor-not-allowed&quot;&gt;
Search
&lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- SVG container + overlay prompt --&gt; &lt;div class=&quot;relative&quot; style=&quot;height: 550px&quot;&gt; &lt;div class=&quot;stv-canvas absolute inset-0 rounded-lg border border-border-default bg-card overflow-hidden&quot;&gt;&lt;/div&gt; &lt;!-- Empty state prompt (sits above the SVG) --&gt; &lt;div class=&quot;stv-prompt absolute inset-0 flex flex-col items-center justify-center text-muted pointer-events-none&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot; class=&quot;opacity-40 mb-3&quot;&gt; &lt;polygon points=&quot;5 3 19 12 5 21 5 3&quot;&gt;&lt;/polygon&gt; &lt;/svg&gt; &lt;p class=&quot;stv-prompt-text text-sm&quot;&gt;Press play to watch the tree being built&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Step description --&gt; &lt;div class=&quot;stv-info mt-3 p-3 rounded-lg border border-border-default bg-card text-sm min-h-[3.5em]&quot;&gt; &lt;p class=&quot;stv-desc text-muted italic&quot;&gt;Add a string to begin building the suffix tree.&lt;/p&gt; &lt;div class=&quot;stv-progress mt-2 hidden&quot;&gt; &lt;div class=&quot;flex justify-between text-xs text-muted mb-1&quot;&gt; &lt;span class=&quot;stv-counter&quot;&gt;Step 0 / 0&lt;/span&gt; &lt;span class=&quot;stv-phase&quot;&gt;&lt;/span&gt; &lt;/div&gt; &lt;div class=&quot;w-full bg-border-default rounded-full h-1&quot;&gt; &lt;div class=&quot;stv-bar bg-primary rounded-full h-1 transition-all duration-200&quot; style=&quot;width: 0%&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;!-- Length warning --&gt; &lt;div class=&quot;stv-warning hidden mt-2 p-2 rounded text-xs text-primary border border-primary/30 bg-primary/10&quot;&gt;
Long strings produce large trees that may be hard to read. Use zoom (scroll) and pan (drag) to navigate.
&lt;/div&gt; &lt;/div&gt; 
&lt;h2 id=&quot;beyond-suffix-trees&quot;&gt;Beyond suffix trees&lt;/h2&gt;
&lt;p&gt;What excites me most is how well this generalizes. The gap between an algorithm on paper and an algorithm in memory has always been one of the hardest parts of learning computer science. Textbooks give you static diagrams. Debuggers give you one node at a time. Neither shows you the whole picture in motion.&lt;/p&gt;
&lt;p&gt;Browser-based rendering, interactive SVGs, and JavaScript engines fast enough to run non-trivial algorithms client-side make it possible to close that gap for almost any data structure. Red-black trees, B-trees, tries, skip lists, hash tables with open addressing: all of them would benefit from this kind of treatment. Not as a replacement for the theory, but as a companion to it. Read the algorithm, then &lt;em&gt;watch&lt;/em&gt; it work.&lt;/p&gt;
&lt;p&gt;There is an obvious question lurking here: why bother learning algorithms at all when you can ask an LLM to write one for you? I think the question misses the more interesting possibility. LLMs are not just code generators; they are learning accelerators. You can ask one to explain a single step of an algorithm, to walk through an edge case, or to generate a diagram of how components interact. When I started working in a new codebase recently, the fastest way for me to build a mental model was not reading code or documentation. It was asking an LLM to produce component and sequence diagrams: a much higher-bandwidth channel for understanding, at least for the way I think.&lt;/p&gt;
&lt;p&gt;That is the real shift. Not that machines can write algorithms so we don’t have to learn them, but that they can teach us in ways that adapt to how each of us actually learns. Through visualizations, through diagrams, through conversation, through whatever representation makes the concept click. This post is one example. The next one might look completely different, tailored to a different person and a different way of thinking.&lt;/p&gt;
&lt;p&gt;We write fewer algorithms from scratch in our day-to-day work than we used to. But we still benefit from understanding them, whether it’s to choose the right data structure, to debug performance issues, or to evaluate tradeoffs. And for those of us who enjoy algorithms for their own sake, the tools for learning them have never been better.&lt;/p&gt;
&lt;p&gt;The original Java suffix tree implementation is &lt;a href=&quot;https://github.com/abahgat/suffixtree&quot;&gt;open source on GitHub&lt;/a&gt;. For the full backstory, see the &lt;a href=&quot;https://www.abahgat.com/project/suffix-tree/&quot;&gt;project page&lt;/a&gt; and the &lt;a href=&quot;https://www.abahgat.com/blog/the-programming-puzzle-that-got-me-my-job/&quot;&gt;story of the programming puzzle&lt;/a&gt; that started it all. Ukkonen’s &lt;a href=&quot;https://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf&quot;&gt;original paper&lt;/a&gt; remains the definitive reference for the algorithm.&lt;/p&gt;</content:encoded></item><item><title>The Velocity Paradox</title><link>https://www.abahgat.com/blog/the-velocity-paradox</link><guid isPermaLink="true">https://www.abahgat.com/blog/the-velocity-paradox</guid><description>AI agents can generate code 100x faster, but for companies stuck in the &quot;Unhappy Middle&quot; — with legacy debt, bespoke frameworks, and zero slack — the bottleneck has shifted from writing code to verifying it. Here&apos;s how engineering leaders can cross the chasm by becoming gardeners, not janitors.</description><pubDate>Mon, 23 Feb 2026 20:30:00 GMT</pubDate><content:encoded>&lt;p&gt;We’ve all been there. You sit down with an AI agent on a Saturday morning to hack on a side project and it feels like magic. Ten minutes in, you are blown away by how quickly the agent can turn even poorly organized thoughts into working prototypes. You feel like you could do this all day.&lt;/p&gt;
&lt;p&gt;And clearly, many of us do: we’re rediscovering our passion for side projects, and every day a thousand bespoke ToDo apps are born, perfectly tailored to the unique needs of their creators.&lt;/p&gt;
&lt;p&gt;At the same time, if you’re in an engineering leadership role, you’re also seeing your stakeholders dabble with agentic coding. They are shipping side-hustles on the weekend, and respectable work applications in an afternoon. Some of them might even look at you with ill-concealed suspicion. They want to know why their “pet feature” is stuck in a two-week cycle when they just whipped up a functional prototype over coffee.&lt;/p&gt;
&lt;p&gt;And they aren’t entirely wrong. AI agents have been writing 100% of my code for several months now. Informed by the wins on my side-projects, I wanted to see how much faster we could build at work. During the holiday break, I spent a few hours having Claude write a non-trivial feature that touched our database, cloud infra, mobile app, and the embedded application that runs on our hardware devices at &lt;a href=&quot;https://www.quilt.com&quot;&gt;Quilt&lt;/a&gt;. What would have taken me a week to write took an afternoon to generate.&lt;/p&gt;
&lt;p&gt;Yet it still took weeks to get it tested and merged.&lt;/p&gt;
&lt;p&gt;It felt like strapping a rocket engine to a tricycle. Exhilarating, sure, but the road ahead is still full of potholes, and there’s a canyon where the bridge used to be. So why isn’t the 100x improvement in how fast AI can &lt;em&gt;generate&lt;/em&gt; code moving the needle on how fast we can &lt;em&gt;ship&lt;/em&gt; features and improvements?&lt;/p&gt;
&lt;p&gt;Coding was never 100% of the job. But for those of us managing legacy debt, AI doesn’t just fail to solve our problems; it collides with them.&lt;/p&gt;
&lt;p&gt;I’ve been at several conferences recently where I met leaders from “AI-native” companies, organizations founded in an age where agentic coding is the baseline. One founder told me they don’t do code reviews at all; their CI pipeline is the reviewer. Another gives agents full control of their production infrastructure. For those of us anchored to a culture that is older than even just two years, these practices feel reckless. Yet even more measured companies are rethinking the fundamentals. OpenAI recently pulled back the curtain with their &lt;a href=&quot;https://openai.com/index/harness-engineering/&quot;&gt;Harness Engineering&lt;/a&gt; article, showing engineering re-architected around AI from the ground up.&lt;/p&gt;
&lt;p&gt;For the rest of us, the gap between “generating code” and “shipping value” is becoming a chasm. We are stuck in the Unhappy Middle, where the cost of code is diminishing rapidly, but the cost of review and verification is skyrocketing.&lt;/p&gt;
&lt;h1 id=&quot;the-unhappy-middle&quot;&gt;The Unhappy Middle&lt;/h1&gt;
&lt;p&gt;To understand why the promise of 100x faster progress thanks to AI still feels like an illusion, we have to look at the two forces we’re being squeezed by.&lt;/p&gt;
&lt;p&gt;On one side, we have the AI-Natives. These are companies and teams founded in the AI era. They have zero legacy debt, they can approach the craft of engineering with an open mind, and they use the same exact “boring” tech stacks the models were trained on. They don’t have to go out of their way to “integrate” AI; they are born out of it. They don’t have to refactor their code to support automated verification, they never knew a world without it.&lt;/p&gt;
&lt;p&gt;On the other side, you have the companies with the slack to reinvent themselves. Shopify’s CEO made headlines when he declared that &lt;a href=&quot;https://x.com/tobi/status/1909251946235437514&quot;&gt;AI proficiency is now a baseline expectation&lt;/a&gt; and that teams must justify why a job can’t be done by AI before requesting headcount. Companies like that (or Google, I bet) can dedicate teams to rearchitect their codebase, tooling and processes and build the scaffolding that is required to make AI work at scale.&lt;/p&gt;
&lt;p&gt;Then, there’s the rest of us. I call it the Unhappy Middle.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We support live products and services, with customers trusting us and depending on us daily. The cost of failure is higher than a toy prototype. Unlike your ToDo app, you can’t just throw an agent at a problem and hope it doesn’t break your production environment.&lt;/li&gt;
&lt;li&gt;We have accumulated technical debt as we were racing towards product/market fit, and yet never had the resources to pay it back. We have to balance work on infrastructure and developer experience with business priorities like opening new product lines. Most of these target ambitious schedules which (you guessed right) require taking on additional technical debt.&lt;/li&gt;
&lt;li&gt;With the age of Zero Interest-Rate Policies well behind us, but not quite with the coffers of a larger company, we always have to be mindful of our runway, are constantly short-staffed and always “do more with less”.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short, we have to balance the technical complexity of an established company with the reality of a startup. Our survival depends on crossing the chasm as quickly as possible. Not every team is here. If your stack is standard and your tests are green, you may already be seeing the gains. But if any of this sounds familiar, the path forward is harder. Here are some examples from my reality.&lt;/p&gt;
&lt;h2 id=&quot;bespoke-frameworks-from-asset-to-dead-weight&quot;&gt;Bespoke Frameworks: from Asset to Dead Weight&lt;/h2&gt;
&lt;p&gt;Before AI, we may have optimized for human speed by building bespoke frameworks, custom boilerplate generators or domain-specific languages and abstractions. For many teams, these were their “secret sauce”: internal abstractions that helped teams move fast in 2022. They came at a price (typically, new engineers have to take some time getting comfortable with them), but they often paid off.&lt;/p&gt;
&lt;p&gt;Today, those clever optimizations are anchors holding us back. AI agents are brilliant at standard React and Python because they’ve seen it a billion times. And, at the same time, they are completely illiterate in our proprietary and opinionated internals. Every time I ask an agent to work in our bespoke code, I’m paying an invisible tax: I spend a third of my time fixing hallucinations because our “clever” code isn’t in anyone’s training set. (I wrote more about why this happens in &lt;a href=&quot;https://www.abahgat.com/blog/the-ghost-in-the-training-set&quot;&gt;The Ghost in the Training Set&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;And you know what’s funny? That’s often why some of the best engineers I know are unimpressed by AI agents: they focus on the last time they saw Claude trip on a gotcha that’s specific to their codebase and ignore the fact that it can build flawless React in the blink of an eye.&lt;/p&gt;
&lt;h2 id=&quot;zero-slack&quot;&gt;Zero Slack&lt;/h2&gt;
&lt;p&gt;We know technical debt is there, we always wanted to increase test coverage, we defer refactoring for testability because we need to fit one more feature before the release cut. We know that frameworks need to be standardized to become “AI-hospitable.” But in the Unhappy Middle, you have zero slack. You’re always racing, either to hit product-market fit or to extend your runway, and “cleaning up” feels like a luxury you can’t afford.&lt;/p&gt;
&lt;p&gt;This creates a painful tradeoff. In a side project, or a non-critical business app, failure is cheap. For a company with a legacy codebase, complex release processes and addressing user-critical needs, the stakes are considerably higher. Without the slack to build automated guardrails, we’re left with manual human review and auditing.&lt;/p&gt;
&lt;p&gt;And that’s where the 100x speed gain from AI goes to die.&lt;/p&gt;
&lt;h1 id=&quot;when-generation-outruns-verification&quot;&gt;When Generation Outruns Verification&lt;/h1&gt;
&lt;p&gt;We often think of the craft of software engineering as composed of several loops, each covering a different stage of the lifecycle, from idea to product. A good visual to illustrate this is the slide below, from a &lt;a href=&quot;https://youtu.be/qi89lhRI8zc?t=207&quot;&gt;talk Addy Osmani gave at LeadDev New York 2025&lt;/a&gt;.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/devloop.CPhBLpoT.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1476&quot; height=&quot;820&quot; srcset=&quot;https://www.abahgat.com/_astro/devloop.CPhBLpoT_Z1g0ytw.webp 400w, https://www.abahgat.com/_astro/devloop.CPhBLpoT_NyH4h.webp 768w, https://www.abahgat.com/_astro/devloop.CPhBLpoT_2ggJxq.webp 1024w, https://www.abahgat.com/_astro/devloop.CPhBLpoT_2gfRf3.webp 1476w, https://www.abahgat.com/_astro/devloop.CPhBLpoT_1LXRS.webp 2040w, https://www.abahgat.com/_astro/devloop.CPhBLpoT_Z1tBj9K.webp 2952w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1476px; max-height: 820px; aspect-ratio: 1.8; width: 100%;&quot; alt=&quot;The development loop&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; From Addy Osmani&apos;s talk at LeadDev New York 2025 &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;At the center is the &lt;em&gt;Inner Loop&lt;/em&gt;: the tight cycle of thinking, coding, building and testing. This is where “flow” happens. Surrounding that is the &lt;em&gt;Submit Loop&lt;/em&gt;, where your code goes through linting and code review, and the &lt;em&gt;Outer Loop&lt;/em&gt;, where it finally gets deployed and gets tested in the real world.&lt;/p&gt;
&lt;p&gt;The promise of AI-assisted engineering is to effectively collapse the Inner Loop. When an agent can “Think” and “Code” a cross-stack feature in a single morning, that center circle feels like it’s spinning at the speed of light.&lt;/p&gt;
&lt;p&gt;But for those of us who are still in the Unhappy Middle, that loop is often broken before it even starts.&lt;/p&gt;
&lt;h2 id=&quot;the-broken-inner-loop&quot;&gt;The Broken Inner Loop&lt;/h2&gt;
&lt;aside class=&quot;not-prose my-8 py-6 px-8 rounded-r-lg border-l-4 border-link bg-link/[0.07]&quot; role=&quot;note&quot; data-astro-cid-rssm4oil=&quot;&quot;&gt; &lt;p class=&quot;text-xl md:text-2xl leading-relaxed text-heading italic pullquote-serif&quot; data-astro-cid-rssm4oil=&quot;&quot;&gt; You were promised AI agents working for you. Instead, you are working for your agents. &lt;/p&gt; &lt;/aside&gt;
&lt;p&gt;The first problem teams are likely to encounter is a broken Inner Loop. Before AI, back in the day when code was expensive to write, tests were the first aspect of a healthy architecture to be sacrificed (or, in the best case scenario, deferred). When we skip writing tests, it’s common to end up with code for which it’s hard to write tests in the long run.&lt;/p&gt;
&lt;p&gt;When you can’t give an agent a deterministic way to verify its own work, the feedback cycle doesn’t feed back into the AI, it feeds back into &lt;strong&gt;you&lt;/strong&gt;. The agent isn’t looping, it’s just throwing code over the wall and waiting for you to tell it what happened.&lt;/p&gt;
&lt;p&gt;In the best scenario you can imagine, the loop is closed by automation. The agent writes code, runs a test, sees the failure and iterates until it’s green. The feedback is a tight, self-correcting circuit.&lt;/p&gt;
&lt;p&gt;Without a way to automate verification, you’re just making a mountain of work for yourself, or accepting to take an enormous amount of risk by shipping code that hasn’t been properly tested.&lt;/p&gt;
&lt;p&gt;You were promised AI agents working for you to help you be more effective; instead, you are working for your agents. Not only is it not fun, it’s also a huge waste of your time because you are 100x slower than a software agent.&lt;/p&gt;
&lt;p&gt;In my world, this isn’t just a metaphor. I feel it physically. At Quilt, we make hardware devices, and you can’t throw prompt engineering at the physical world. If a test requires me to get up, walk to a test bench and manually press a button, the inner loop isn’t just broken; it’s wide open.&lt;/p&gt;
&lt;p&gt;And there are even worse consequences downstream.&lt;/p&gt;
&lt;h2 id=&quot;the-slowing-submit-loop&quot;&gt;The Slowing Submit Loop&lt;/h2&gt;
&lt;p&gt;Before AI agents were this capable, the high cost of &lt;em&gt;writing&lt;/em&gt; code carried a hidden benefit. If an engineer spent two days wrestling with a complex feature, they effectively distilled a lot of context information into their brain. By the time they put a change up for review, the author was the deepest expert on those 200 lines of code.&lt;/p&gt;
&lt;p&gt;That’s not how it works today.&lt;/p&gt;
&lt;p&gt;As wonderful as the democratizing effect of AI agents is (they enable engineers to contribute well beyond their historical area of expertise), it comes with downsides.&lt;/p&gt;
&lt;p&gt;If an agent can’t automatically verify its changes, and the author is not the most experienced engineer in the area affected by a change, the bulk of the burden of audit and review will shift to the reviewer.&lt;/p&gt;
&lt;p&gt;On the average team, code reviews are assigned to the most experienced engineers in a given area or domain. In this new world, these folks are getting overloaded with more code to review. Worse, they can no longer assume that the author has the same depth of knowledge about the code that reviewers historically could take for granted.&lt;/p&gt;
&lt;p&gt;At the extreme, this has multiple effects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Because the agent did the heavy lifting, the human author may have a shallower understanding of the “why” behind specific implementation choices.&lt;/li&gt;
&lt;li&gt;The reviewer is now receiving 10x more code, but with 10x less intent provided by the author. If the reviewer didn’t (or couldn’t) do a thorough review themselves, it’s 10x more code reviews of a higher intensity. Think more of a forensic audit than a style check.&lt;/li&gt;
&lt;li&gt;In a legacy codebase with bespoke frameworks, this can be extremely challenging. If neither the author nor the reviewer fully understands the “clever” choices the AI made, they can’t distinguish between valuable additions and hallucinations, and therefore are taking a high risk shipping this to production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical consequences are tangible. Code ends up spending more time waiting for review than in development (this is what happened to my proof of concept I mentioned earlier). Your most experienced engineers struggle to be productive themselves because they are drowning in code reviews.&lt;/p&gt;
&lt;p&gt;But the most worrisome part is what this does at an emotional level.&lt;/p&gt;
&lt;h1 id=&quot;from-craftspeople-to-janitors&quot;&gt;From Craftspeople to Janitors&lt;/h1&gt;
&lt;p&gt;If we take the patterns above to the extreme and let them fester without fixing them, then we are taking on a huge organizational risk by turning our most senior engineers into Janitors.&lt;/p&gt;
&lt;p&gt;Instead of going to a challenging workday where, at the end, we experience the joy of having created something new, we now have to pore over someone (or, rather, something) else’s code to spot issues and problems. Some engineers feel like they are being paid to clean up AI hallucinations.&lt;/p&gt;
&lt;p&gt;This can be deeply demotivating. No one likes being a linear bottleneck downstream of a stage that is accelerating at exponential speed. This is even more difficult at the speed this shift is happening, as many people are mourning the loss of the craft, made worse by simplistic takes about how the world of tomorrow needs fewer engineers.&lt;/p&gt;
&lt;p&gt;I still deeply enjoy coding but I recognize that, even in the best of days, a lot of the code I wrote was boilerplate needed to wire together different application components. A very common micro-kitchen joke from my time at Google was that we were all just highly-compensated Protocol Buffer translators.&lt;/p&gt;
&lt;p&gt;We miss the 20% of the code we used to write that was high-leverage and intellectually interesting, and forget the other 80% that was toilsome and repetitive.&lt;/p&gt;
&lt;h1 id=&quot;from-janitors-to-gardeners&quot;&gt;From Janitors to Gardeners&lt;/h1&gt;
&lt;p&gt;If you treat every AI-generated PR like a chore to be cleaned up, you are a Janitor. To move fast in a legacy codebase, we need a considerable change in mindset. If you allow me another metaphor, we need to start treating our codebase less like a perfect jewel to polish and more like a plot of land to tend to.&lt;/p&gt;
&lt;p&gt;I’ve been thinking about this metaphor for a while. As you scale an organization, you can’t afford to micromanage; you provide structure and support so that decisions happen organically, aligned to what the business needs. The same applies to codebases.&lt;/p&gt;
&lt;p&gt;Playing into the metaphor, a gardener may focus their attention on a few things:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tending the Soil&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hospitable Ground&lt;/strong&gt; — Transforming AI-Hostile codebases into an AI-Hospitable playing field requires investing in reducing technical debt, so that AI can’t hide behind it. It may mean moving away from bespoke patterns that routinely trip up agents, or making them work reliably. It means standardizing on a well-defined and documented set of abstractions, instead of having 3 different ways to set up an API server because we never finish migrations every time we deprecate an old pattern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nutrient-Rich Soil&lt;/strong&gt; — Agents are great at brute-forcing their way to a workable solution, but very often they struggle because the codebase lacks information beyond the code itself. Code written in haste often lacks documentation about “Intent” and the “Why” we made decisions. If we don’t expose context about tradeoffs and historical decisions, our agents are operating with limited information. Well structured &lt;code&gt;agents.md&lt;/code&gt; files are a good start. Checking in architectural guidelines and making them discoverable is increasingly paying off. Ironically, if you keep your design docs locked in Google Docs, your agent is blind to them (hey Google, when can we have MCP access to Google Docs?)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Scaffolding and Direction&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scaffolding&lt;/strong&gt; — You don’t tell plants how to grow and expect them to listen; you provide scaffolding and support. In software, this can be types, interfaces and architectural boundaries. Well crafted designs that reduce coupling and abstract complexity behind well-defined interfaces are how you give agents a way to grow that is aligned to what you need.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilience&lt;/strong&gt; — Automated tests, lint checks and verifications are much more helpful for AI agents than they are to humans, as they enable both faster iteration speed and more confidence in the review stage of the submit loop. In the gardening metaphor, this is akin to the sturdy fencing that protects your plants from critters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I find it ironic that many of the principles above are ones that practitioners have been advocating for under the banner of clean code, test-driven development and many others. We might callously shrug at the idea that we struggled to adopt them for the sake of our human co-workers and are now prioritizing them for the sake of our AI-agents. But the truth is that in the last decade, writing effective tests and good documentation cost us time: the time to think about them, and the time to type them. With AI agents being this capable, the typing cost is approaching zero. What remains is the thinking, and that was always the valuable part.&lt;/p&gt;
&lt;h1 id=&quot;building-the-dark-factory&quot;&gt;Building the Dark Factory&lt;/h1&gt;
&lt;aside class=&quot;not-prose my-8 py-6 px-8 rounded-r-lg border-l-4 border-link bg-link/[0.07]&quot; role=&quot;note&quot; data-astro-cid-rssm4oil=&quot;&quot;&gt; &lt;p class=&quot;text-xl md:text-2xl leading-relaxed text-heading italic pullquote-serif&quot; data-astro-cid-rssm4oil=&quot;&quot;&gt; Our job is no longer to write the code. It’s to build the factory that builds the code. &lt;/p&gt; &lt;/aside&gt;
&lt;p&gt;By now, it should be obvious that if we use AI only to automate the “Coding” stage of the development loop, we may not only struggle to make our team more effective, we may even hurt their effectiveness.&lt;/p&gt;
&lt;p&gt;In the same talk by Addy Osmani I referenced earlier, he goes on to show several areas where AI can be effectively adopted to improve developer experience. In my day-to-day work, I’ve had considerable success using AI agents to troubleshoot bug reports and infrastructure alerts from our production fleet. The gains are real.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/devloop-annotated.DskcXKBh.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1570&quot; height=&quot;873&quot; srcset=&quot;https://www.abahgat.com/_astro/devloop-annotated.DskcXKBh_ZlcDUy.webp 400w, https://www.abahgat.com/_astro/devloop-annotated.DskcXKBh_20D2bA.webp 768w, https://www.abahgat.com/_astro/devloop-annotated.DskcXKBh_Z28MBIu.webp 1024w, https://www.abahgat.com/_astro/devloop-annotated.DskcXKBh_Z2wVHMf.webp 1570w, https://www.abahgat.com/_astro/devloop-annotated.DskcXKBh_Z2aAzMm.webp 2040w, https://www.abahgat.com/_astro/devloop-annotated.DskcXKBh_UUgiS.webp 3140w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1570px; max-height: 873px; aspect-ratio: 1.7983963344788088; width: 100%;&quot; alt=&quot;The development loop, annotated&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; From Addy Osmani&apos;s talk at LeadDev New York 2025 &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;There is a growing conversation in engineering circles about “&lt;a href=&quot;https://www.danshapiro.com/blog/2026/01/the-five-levels-from-spicy-autocomplete-to-the-software-factory/&quot;&gt;Dark Factories&lt;/a&gt;”: fully automated systems that run without human intervention. In the age of AI, our job is no longer to write the code; it’s to &lt;strong&gt;build the factory that builds the code.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Some high-leverage areas to start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Verification Machine&lt;/strong&gt; — Good test infrastructure should be the top priority. Well-written tests enable AI-agents to have much faster inner loops, but they also greatly help with faster code reviews. With good test scaffolding, you don’t just ask “Will this code work in this scenario?” You can ask an agent to demonstrate the expected behavior via a unit test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Address common tripping hazards for agents&lt;/strong&gt; — You likely have a few areas where agents routinely struggle. Don’t just scoff when that happens, and use it to say “AI isn’t quite there yet”. Ask yourself &lt;em&gt;why&lt;/em&gt; agents are struggling. Is it because of inconsistent patterns? Lack of context or documentation? Because your bespoke framework requires 1 year of experience &lt;em&gt;in your own codebase&lt;/em&gt; to master? Making sure agents don’t make the same mistake twice should be part of our responsibilities.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reducing human dependencies for mechanical tasks&lt;/strong&gt; — Invest in building reliable automated end to end tests that rely on production-like observability to spot issues and regressions. Wherever manual testing is required, ask yourself “what would it take for this test to happen automatically?” In a hardware company like Quilt, this means augmenting our ability to perform more tests in software.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Lights-Out Goal&lt;/strong&gt; — Aim to have a “Submit Loop” so robust that if tests pass and the architectural boundaries are respected, the code is “shippable” by default. Even if that goal feels unrealistic (e.g. for code that is security-critical or that runs on devices that are hard to recover), ask yourself “What would it take for me to be 100% confident in a change without needing to review it?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A word of warning: don’t confuse building the factory with building more features. If you ship 10x more features without correspondingly improving your infrastructure, you’re taking on a compounding liability. If AI agents today are enabling you to move even just a bit faster than yesterday, aim to put some of those velocity gains towards your scaffolding, instead of putting everything on more features.&lt;/p&gt;
&lt;h1 id=&quot;crossing-the-chasm&quot;&gt;Crossing the Chasm&lt;/h1&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/chasm.r78RJefF.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;3168&quot; height=&quot;1344&quot; srcset=&quot;https://www.abahgat.com/_astro/chasm.r78RJefF_Znct6D.webp 400w, https://www.abahgat.com/_astro/chasm.r78RJefF_Z1sM3lB.webp 768w, https://www.abahgat.com/_astro/chasm.r78RJefF_2f4awa.webp 1024w, https://www.abahgat.com/_astro/chasm.r78RJefF_Z1uMLRh.webp 2040w, https://www.abahgat.com/_astro/chasm.r78RJefF_ZTp7TR.webp 3168w, https://www.abahgat.com/_astro/chasm.r78RJefF_ZXFBLp.webp 6336w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 3168px; max-height: 1344px; aspect-ratio: 2.357142857142857; width: 100%;&quot; alt=&quot;A rocket-powered bicycle approaching a broken bridge over a canyon&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;  &lt;/figure&gt;
&lt;aside class=&quot;not-prose my-8 py-6 px-8 rounded-r-lg border-l-4 border-link bg-link/[0.07]&quot; role=&quot;note&quot; data-astro-cid-rssm4oil=&quot;&quot;&gt; &lt;p class=&quot;text-xl md:text-2xl leading-relaxed text-heading italic pullquote-serif&quot; data-astro-cid-rssm4oil=&quot;&quot;&gt; If the smartest AI in the world can’t understand your code, it might not be the AI’s fault. &lt;/p&gt; &lt;/aside&gt;
&lt;p&gt;The Unhappy Middle is a trap, but it’s also an opportunity to rethink what engineering leadership looks like.&lt;/p&gt;
&lt;p&gt;This requires a fundamental shift in our ego as developers. Instead of ‘pwning’ the agent every time it trips on our proprietary abstractions, we need to ‘own’ our codebase and make it more AI-hospitable. If the smartest AI in the world can’t understand your code, it might not be the AI’s fault, but it might be a sign that our “cleverness” has become our biggest liability.&lt;/p&gt;
&lt;p&gt;If we don’t cross the chasm quickly and change our mindset about how we write software, we risk being buried under our own AI-generated slop. The first step is to stop prioritizing just features as our primary output and start prioritizing the speed and accuracy of the factory.&lt;/p&gt;
&lt;p&gt;It is notoriously hard to get organizational buy-in to address technical debt. The key is to reframe: this isn’t about “cleaning up” to pay off debt, it’s about investing in tooling to accomplish 10x velocity.&lt;/p&gt;
&lt;p&gt;And even then, there are harder questions ahead. If you actually succeed in building the “factory,” you’ll quickly find that the technical bottleneck has evaporated, only to leave you with an organizational one. A 10x software factory is effectively useless if it’s embedded in a 1x decision-making process. And it is possible that we are approaching a &lt;a href=&quot;https://en.wikipedia.org/wiki/Great_Filter&quot;&gt;Great Filter&lt;/a&gt;-like event for companies in the business of software — one that separates those who adapt from those who drown. But those are topics for another day.&lt;/p&gt;
&lt;p&gt;For now, the goal is clear: stop just auditing lines of code and start building the systems that define the future of our industry.&lt;/p&gt;
&lt;p&gt;Let us begin.&lt;/p&gt;
&lt;div class=&quot;not-prose flex gap-3 p-4 rounded-lg border bg-surface border-border-default text-default  alert alert-note&quot;&gt; &lt;svg width=&quot;1em&quot; height=&quot;1em&quot; class=&quot;w-5 h-5 flex-shrink-0 mt-2&quot; data-icon=&quot;tabler:info-circle&quot;&gt;   &lt;symbol id=&quot;ai:tabler:info-circle&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;path d=&quot;M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0m9-3h.01&quot;/&gt;&lt;path d=&quot;M11 12h1v4h1&quot;/&gt;&lt;/g&gt;&lt;/symbol&gt;&lt;use href=&quot;#ai:tabler:info-circle&quot;&gt;&lt;/use&gt;  &lt;/svg&gt; &lt;div&gt; &lt;p class=&quot;font-bold mb-1&quot;&gt;Update — March 2026&lt;/p&gt; &lt;div class=&quot;text-sm prose-p:my-0 prose-p:first:mt-0 prose-p:last:mb-0 [&amp;amp;&gt;p]:my-0&quot;&gt; &lt;p&gt;I explored the “1x decision-making process” problem further in &lt;a href=&quot;https://www.abahgat.com/blog/permission-structure&quot;&gt;Permission Structure&lt;/a&gt;.&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt;</content:encoded></item><item><title>The Ghost in the Training Set</title><link>https://www.abahgat.com/blog/the-ghost-in-the-training-set</link><guid isPermaLink="true">https://www.abahgat.com/blog/the-ghost-in-the-training-set</guid><description>LLMs have statistical momentum: even when they know a new standard like Streamable HTTP exists, they often revert to the legacy patterns they saw most in training. Here is how to use &quot;strong anchors&quot; and &quot;zero-prompt pruning&quot; to keep your agentic systems from being haunted by 2024.</description><pubDate>Sat, 14 Feb 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Over the last several weeks, I’ve had to spend time setting up Model Context Protocol (MCP) servers. As the ecosystem matures, it is already navigating its first major paradigm shifts. Specifically, in early 2025, the recommended transport for MCP over HTTP shifted from Server-Sent Events (SSE) to &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http&quot;&gt;&lt;strong&gt;Streamable HTTP&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To my surprise, the agents I use most (Gemini and Claude) kept reverting to SSE. They were well “aware”, at least as much as a machine could be, that Streamable HTTP was the new standard (they could competently answer questions about it) but they were haunted by the &lt;strong&gt;statistical momentum&lt;/strong&gt; of their own training data. When it came time to actually generate code, they defaulted to the pattern they had seen thousands of times before.&lt;/p&gt;
&lt;h2 id=&quot;the-invisible-weight-of-training-bias&quot;&gt;The Invisible Weight of Training Bias&lt;/h2&gt;
&lt;p&gt;Taking a step back, this makes perfect sense: LLMs don’t just “read” instructions in a traditional sense: they weigh them against their internal probability map. If most of the MCP implementations they had seen were built over SSE, that gives them a huge bias in that direction.&lt;/p&gt;
&lt;p&gt;Once I started noticing this pattern, I had found it more and more often: LLMs seem to struggle more with bleeding edge patterns and technologies (again, their training dataset has more examples built on deprecated patterns than newer standards).&lt;/p&gt;
&lt;p&gt;This is a sneaky pattern, because we don’t naturally think about how old (or new) a model’s training set is, so we can’t realize this is happening unless we pay attention. If you’re working on a bleeding edge domain and you’re not careful, you may find yourself with an agent offering you a beautiful implementation that is actually a frozen snapshot of &lt;em&gt;last year&lt;/em&gt;’s best practices.&lt;/p&gt;
&lt;p&gt;The challenge grows with the uniqueness of your environment. This problem is even worse with codebases that adopt bespoke frameworks and patterns for which there is no published precedent. Agents thrive on &lt;em&gt;Common Knowledge&lt;/em&gt;, and they struggle with &lt;em&gt;Private Context&lt;/em&gt;. When we use bespoke patterns, we are essentially moving the agent into a zero-shot environment without even realizing it. The result is a performance degradation that looks like a “dumb” model but it is actually a lack of statistical grounding.&lt;/p&gt;
&lt;h2 id=&quot;from-prompting-to-infrastructure&quot;&gt;From Prompting to Infrastructure&lt;/h2&gt;
&lt;p&gt;You may be tempted to try to overcome this through prompting, and try to give strong instructions to anchor your agent towards the new standard by including strong language in your prompt (&lt;code&gt;ALWAYS use Streamable HTTP when implementing MCP services&lt;/code&gt;). You need strong anchors to overcome strong biases. But prompts are often lossy, inconsistent and error-prone.&lt;/p&gt;
&lt;p&gt;A more sustainable strategy is to start including these guardrails into your &lt;code&gt;agents.md&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; files, or even better in tooling infrastructure. For example, Claude includes an &lt;code&gt;/mcp-builder&lt;/code&gt; skill, which serves as a specialized instruction package anchored on the most recent standards, ensuring you land with a well-functioning implementation that overcomes the inherent bias in the models. In contrast, if you tried building an MCP server with Gemini now you may find yourself surprised by a perfectly functional implementation built on the deprecated 2024 pattern.&lt;/p&gt;
&lt;h2 id=&quot;the-trap-of-contextual-debt&quot;&gt;The Trap of “Contextual Debt”&lt;/h2&gt;
&lt;p&gt;Just like code accumulates technical debt, continuously adding to &lt;code&gt;agents.md&lt;/code&gt; without ever cleaning up leads to “contextual debt”. Over time, these files become bloated with a mountain of “Don’t do X” or “Remember Y.” Even worse, because you can have &lt;code&gt;agents.md&lt;/code&gt; files scattered through your repo, and other &lt;code&gt;.md&lt;/code&gt; files as documentation, you can find yourself with clashing instructions that throw agents for a loop in ways that are surprisingly difficult to detect and remedy.&lt;/p&gt;
&lt;p&gt;We are reaching a point where our “Instruction Budget” is as important as our compute budget. If you have clashing instructions across multiple &lt;code&gt;.md&lt;/code&gt; files, you’re not just wasting tokens, you’re creating “hallucination traps” that are far more expensive to debug than a standard syntax error.&lt;/p&gt;
&lt;p&gt;Here are a few things that worked well for me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Progressive Disclosure&lt;/strong&gt;: Borrowing from the &lt;a href=&quot;https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf&quot;&gt;Claude skills playbook&lt;/a&gt;, instead of having a giant instruction file, use a modular approach (e.g., a &lt;code&gt;docs/MCP_STANDARDS.md&lt;/code&gt; file linked from your root &lt;code&gt;agents.md&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The “Zero-Prompt Test” Stress Test&lt;/strong&gt;: Periodically run an agent on your project with a blank instruction file (especially after significant model updates). If performance remains stable, the underlying training set has likely caught up to the new standard. At that point, your manual instructions are no longer necessary; they are cruft. Delete them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ownership of Configs&lt;/strong&gt;: Treat agent configurations with as much rigor as a CI/CD pipeline. Obsolete agent instructions have even more impact on your velocity than obsolete documentation, and ironically, up-to-date documentation is now more precious than ever.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With the rapid pace at which things are evolving, I would not be surprised if in a year, half of these strategies would not be necessary as agents get better. And perhaps they will be superseded by a new set of practices.&lt;/p&gt;
&lt;h2 id=&quot;conclusion-managing-the-agents-ai-memory&quot;&gt;Conclusion: Managing the Agent’s AI “Memory”&lt;/h2&gt;
&lt;p&gt;Regardless of what you might think about the tropes around “software engineering being dead”, it is undeniable that the focus of our job is moving away (or perhaps upward) from writing code.&lt;/p&gt;
&lt;p&gt;As we spend more effort managing attention and memory of our agents, in the most sustainable agentic systems, instructions and scaffolding will be pruned as ruthlessly, if not more so, than the code itself.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;In this post, we’ll reference only &lt;code&gt;agents.md&lt;/code&gt;. Hopefully we’re not far from the day where we don’t need to maintain a separate configuration for Claude. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Receiving Feedback Is A Skill</title><link>https://www.abahgat.com/blog/receiving-feedback</link><guid isPermaLink="true">https://www.abahgat.com/blog/receiving-feedback</guid><description>Delivering feedback is a critical part of my day job as a manager at Google. However, it took me a while to realize that receiving feedback is one of the skills that helped me grow the most in my career. Here a few things I learned in the process.</description><pubDate>Tue, 25 Aug 2020 19:35:24 GMT</pubDate><content:encoded>&lt;p&gt;Delivering feedback is a critical part of my day job as a manager at Google. However, it took me a while to realize that &lt;em&gt;receiving&lt;/em&gt; feedback is one of the skills that helped me grow the most in my career.&lt;/p&gt;
&lt;p&gt;For many of us, our job is the first setting where we receive developmental feedback from people other than our parents or teachers. That experience may be quite shocking.&lt;/p&gt;
&lt;p&gt;I still remember the first time I got professional feedback early in my career. I remember almost every single word that my manager chose to use.&lt;/p&gt;
&lt;p&gt;What I remember even more vividly though is the strong reaction that feedback caused in me. Within seconds, I got defensive, I felt like I was being criticized, attacked, unappreciated. I heard what they were trying to tell me, but something inside me kept translating that into a personal criticism. A statement about how I, personally, fell short of expectations.&lt;/p&gt;
&lt;p&gt;Good feedback sounds like “here’s one thing you can do better next time”. Better feedback sounds like “here’s one thing that you could do differently to achieve a greater result”.&lt;/p&gt;
&lt;p&gt;Embracing that mindset allowed me to accept, process and build on feedback. While I can’t say I &lt;em&gt;prefer&lt;/em&gt; criticism over praise, constructive feedback no longer makes me uncomfortable. Instead, I actively seek it.&lt;/p&gt;
&lt;p&gt;Changing my mindset around feedback required me to make two key changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stop doing things that hurt my ability to improve&lt;/li&gt;
&lt;li&gt;start doing things that help build on what I hear&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;things-i-stopped-doing&quot;&gt;Things I Stopped Doing&lt;/h2&gt;
&lt;h3 id=&quot;taking-it-personally&quot;&gt;Taking It Personally&lt;/h3&gt;
&lt;p&gt;The main reason I had a difficult time processing feedback is the fact that I often took it personally.&lt;/p&gt;
&lt;p&gt;When receiving feedback about something I did, I often read it as feedback about &lt;em&gt;me&lt;/em&gt;. Oftentimes, that was not the intention.&lt;/p&gt;
&lt;p&gt;Instead of hearing “this email was hard to understand”, I heard “you do not communicate effectively”. When the other party was saying “this piece of code is brittle”, I was hearing “you are a lousy programmer”.&lt;/p&gt;
&lt;p&gt;I often ended up reacting defensively. I was unable to hear and processing the actual message I needed to receive.&lt;/p&gt;
&lt;p&gt;Most developmental feedback will naturally trigger a defensive attitude. That prevents us from getting the full value of what the other person is trying to tell us. &lt;em&gt;We need to make a conscious effort to not jump to defensive mode, and rather engage in active listening&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&quot;arguing-with-feedback&quot;&gt;Arguing With Feedback&lt;/h3&gt;
&lt;p&gt;Even worse than taking feedback personally, I sometimes found myself wanting to argue with the person delivering it. I wanted to explain why I disagreed with what they were seeing or try to convince them that they were wrong.&lt;/p&gt;
&lt;p&gt;In most cases, arguing with feedback is pointless. Take an example from many years ago.&lt;/p&gt;
&lt;p&gt;A colleague approached me and told me “I think the comments you left in this review were too harsh”.&lt;/p&gt;
&lt;p&gt;Now, if they cared enough to bring up this feedback, perhaps they were not the only ones. Or maybe my communication style could have had an unintended effect on some people, some time.&lt;/p&gt;
&lt;p&gt;Yes, I could have argued with my colleague, perhaps even convince them that my tone was not that bad. Winning the argument might even have felt better.&lt;/p&gt;
&lt;p&gt;That would not have changed the my comments did trigger a negative reaction for them. Quite likely, others might have had the same reaction. Knowing that, having that awareness, made me more thoughtful when writing review comments. I can tell they were better received from that moment on.&lt;/p&gt;
&lt;p&gt;Arguing with people who are trying to give us feedback, does not help us. Eventually, people will shy away from telling us where we can improve. It leads us to us working with less information about what we can do to get better. In the long run, we miss out on a significant opportunity.&lt;/p&gt;
&lt;h2 id=&quot;things-i-learned-to-do-instead&quot;&gt;Things I Learned To Do Instead&lt;/h2&gt;
&lt;h3 id=&quot;being-thankful&quot;&gt;Being Thankful&lt;/h3&gt;
&lt;p&gt;A friend of mine once shared a quote that sounded like “feedback is a gift”&lt;/p&gt;
&lt;p&gt;Good feedback is thoughtful and timely. Often, it is as difficult to deliver as it is to receive. It is especially difficult for people we are not very close with.&lt;/p&gt;
&lt;p&gt;Any yet, some people choose to take a risk. They let us know where we can do better. They do that knowing well that we may feel hurt by what they say.&lt;/p&gt;
&lt;p&gt;Because of this, the first thing I do when receiving feedback is thank whoever is giving it. I thank them because they took a risk and did something uncomfortable. I also thank them because what they are telling me has the potential of making me much better.&lt;/p&gt;
&lt;p&gt;Good feedback allows us to identify growth areas. Areas where we could invest more to get better at something we have been trying to do. Even those of us that have good self-awareness often need to work hard to find where they need to improve the most.&lt;/p&gt;
&lt;p&gt;If someone is coming to us with feedback, they may be sparing us a lot of hard work required to identify areas of improvement.&lt;/p&gt;
&lt;p&gt;The least we can do is thank them profusely for the gift they just gave us and get to work.&lt;/p&gt;
&lt;h3 id=&quot;following-up&quot;&gt;Following Up&lt;/h3&gt;
&lt;p&gt;Whenever I receive feedback about something I can improve and want to work on, I note it down. Over time, this list becomes my feedback log.&lt;/p&gt;
&lt;p&gt;Keeping a list of the items I am trying to get better at is a way to hold myself accountable. I go through this feedback log every few weeks and reflect on the progress (or lack of progress) I have seen so far.&lt;/p&gt;
&lt;p&gt;This helps me making sure I make the most of the feedback I was generously given and use it to gradually get better. I try to spend some time every week to work on some of the most important items on the feedback log.&lt;/p&gt;
&lt;p&gt;Doing this helps me well beyond the result of addressing feedback. It also helps me ground my identity as someone who can accept feedback gracefully and use it as a tool to keep growing every day.&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;A few simple changes in perspective helped me change my view on feedback. I went from seeing it as a threat to my own self-worth to a stepping stone to become a better version of myself.&lt;/p&gt;
&lt;p&gt;The results of this attitude compound over time as I keep focusing my energy towards addressing the most critical feedback items.&lt;/p&gt;</content:encoded></item><item><title>Programming Machine Learning</title><link>https://www.abahgat.com/blog/programming-machine-learning</link><guid isPermaLink="true">https://www.abahgat.com/blog/programming-machine-learning</guid><description>A book written with developers in mind, covering Machine Learning with a hands-on approach. Each new topic is introduced by laying out a real world problem, guiding readers through implementing a working solution based on ML algorithms and then explaining the theoretical foundations in a very accessible way.</description><pubDate>Mon, 04 May 2020 14:47:47 GMT</pubDate><content:encoded>&lt;p&gt;I just received my copy of &lt;a href=&quot;https://amzn.to/3bUyn5a&quot;&gt;Programming Machine Learning&lt;/a&gt;, a book by &lt;a href=&quot;https://twitter.com/nusco&quot;&gt;Paolo Perrotta&lt;/a&gt;. I had the pleasure of being one of the technical reviewers of the draft and, while this is not the first book I read about Machine Learning, I must say it became one of my favorites.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.abahgat.com/_astro/programming-ml.yXEqwxlW_1oikSk.webp&quot; alt=&quot;The book cover&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;300&quot; height=&quot;360&quot;&gt;&lt;/p&gt;
&lt;p&gt;Paolo promises, at the beginning of the book, to write a book meant for developers, and he delivers on that promise.&lt;/p&gt;
&lt;p&gt;In his words,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is the book I missed when I got started with machine learning: &lt;mark&gt;an introduction for developers, written in our own language&lt;/mark&gt;. After reading it, you’ll be comfortable with the fundamentals, and able to write machine learning programs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Programming Machine Learning&lt;/em&gt; is a book that teaches the foundations of ML by walking the reader through the process of implementing working solutions for a few concrete and specific use cases, such as predicting sales volume for a pizzeria, recognizing hand-written digits or classifying images.&lt;/p&gt;
&lt;p&gt;Each chapter introduces a challenge, lays out the foundations of a technical implementation and explains the theoretical background behind the techniques adopted.&lt;/p&gt;
&lt;p&gt;As a result, the book is much easier to follow than many others on this subject: even when diving deeper into the technical or mathematical aspects of any of the topics covered, the reader is able to build on the empirical intuition that comes from having implemented ML algorithms and having seen them in action. Every chapter is engaging, starting from the first ones, about trying to predict pizza sales via linear regression and simple perceptrons, to the last ones, leveraging &lt;a href=&quot;https://keras.io/&quot;&gt;Keras&lt;/a&gt; to classify images.&lt;/p&gt;
&lt;p&gt;I found the overall approach quite novel and refreshing. I would definitely recommend &lt;em&gt;Programming Machine Learning&lt;/em&gt;, especially if you are the type of engineer who generally enjoys learning by doing.&lt;/p&gt;</content:encoded></item><item><title>The programming puzzle that landed me my job</title><link>https://www.abahgat.com/blog/the-programming-puzzle-that-got-me-my-job</link><guid isPermaLink="true">https://www.abahgat.com/blog/the-programming-puzzle-that-got-me-my-job</guid><description>And how solving it required a truly full-stack solution, covering web development, data structures and memory optimization</description><pubDate>Tue, 01 Oct 2019 03:35:33 GMT</pubDate><content:encoded>&lt;p&gt;Back in 2011, as I was getting a bored with my job and I started looking for new options. During my search, my friend Daniele (with whom I had built &lt;a href=&quot;https://www.abahgat.com/project/novlet&quot;&gt;Novlet&lt;/a&gt; and &lt;a href=&quot;https://www.abahgat.com/project/bitlet&quot;&gt;Bitlet&lt;/a&gt; years before) forwarded me a link to the careers page of the company he was working for at the time, &lt;a href=&quot;https://en.wikipedia.org/wiki/ITA_Software&quot;&gt;ITA Software&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While Google was in the process of acquiring ITA Software, ITA still had a number of open positions they were looking to hire for. Unlike Google, however, they required candidates to &lt;a href=&quot;https://web.archive.org/web/20111012115624/http://itasoftware.com/careers/work-at-ita/hiring-puzzles.html&quot;&gt;solve a programming challenge&lt;/a&gt; before applying to engineering roles.&lt;/p&gt;
&lt;p&gt;The problems to solve were surprisingly varied, ranging from purely algorithmic challenges to more broadly scoped problems that still required some deep technical insight. As I browsed through the options, I ended up settling on a problem that intrigued me because I thought it resembled a problem I might one day wanted to solve in the real world and seemed to try to test both the breadth of my knowledge (it required good full stack skills) as well as my understanding of deep technical details.&lt;/p&gt;
&lt;p&gt;I have good memories of the time I spent investigating this problem and coming up with a solution. When I was done, I had learned about a new class of data structures (suffix trees), gained a deeper understanding of Java’s internals. A year later, I got a job offer due in part to this puzzle.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/ita-puzzle.CJ0ogDvj.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1366&quot; height=&quot;1112&quot; srcset=&quot;https://www.abahgat.com/_astro/ita-puzzle.CJ0ogDvj_hqqIh.webp 400w, https://www.abahgat.com/_astro/ita-puzzle.CJ0ogDvj_Z1hIlW5.webp 768w, https://www.abahgat.com/_astro/ita-puzzle.CJ0ogDvj_26c0Yf.webp 1024w, https://www.abahgat.com/_astro/ita-puzzle.CJ0ogDvj_ZSL0TB.webp 1366w, https://www.abahgat.com/_astro/ita-puzzle.CJ0ogDvj_ZO4CEz.webp 2040w, https://www.abahgat.com/_astro/ita-puzzle.CJ0ogDvj_9cYC1.webp 2732w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1366px; max-height: 1112px; aspect-ratio: 1.2284172661870503; width: 100%;&quot; alt=&quot;Instant Search puzzle brief on itasoftware.com (as of 2011)&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Instant Search puzzle brief on itasoftware.com (as of 2011) &lt;/figcaption&gt; &lt;/figure&gt;
&lt;h4 id=&quot;the-problem-statement&quot;&gt;The Problem Statement&lt;/h4&gt;
&lt;p&gt;The brief for the challenge was the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Instant Search&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Write a Java web application which provides “instant search” over properties listed in the National Register of Historic Places. Rather than waiting for the user to press a submit button, your application will dynamically update search results as input is typed. We provide the file &lt;code&gt;nrhp.xml.gz&lt;/code&gt;, which contains selected information from the register’s database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database&lt;/strong&gt; The key component of your server-side application is an efficient, in-memory data structure for looking up properties (written in pure Java). A good solution may take several minutes to load, but can answer a query in well under 0.1 ms on a modern PC. (Note that a sequential search of all properties is probably too slow!) An input matches a property if it is found at any position within that property’s names, address, or city+state. Matches are case-insensitive, and consider only the characters A-Z and 0-9, e.g. the input “mainst” matches “200 S Main St” and “red” matches “Lakeshore Dr.” Note that the server’s JVM will be configured with 1024M maximum heap space. Please conform to the interfaces specified in &lt;code&gt;nrhp.jar&lt;/code&gt; when creating your database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Servlet&lt;/strong&gt; Your servlet should accept an input string as the request parameter to a GET request. Results should include the information for a pre-configured number of properties (e.g. 10), the total number of matches which exist in the database, and the time taken by your search algorithm. Your servlet should be stateless, ie. not depend on any per-user session information. Paginate your additional results as a bonus!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client&lt;/strong&gt; Your web page should access the servlet using JavaScript’s XMLHttpRequest object. As the user types, your interface should repeatedly refine the list of search results without refreshing the page. Your GUI does not have to be complicated, but should be polished and look good.&lt;/p&gt;
&lt;p&gt;Please submit a WAR file, configuration instructions, your source code, and any comments on your approach. Your application will be tested with Tomcat on Sun’s 64-bit J2SE and a recent version of Firefox.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/screenshot.DL2AXHKQ.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;411&quot; height=&quot;694&quot; srcset=&quot;https://www.abahgat.com/_astro/screenshot.DL2AXHKQ_1cE7rq.webp 400w, https://www.abahgat.com/_astro/screenshot.DL2AXHKQ_Z2isL9m.webp 411w, https://www.abahgat.com/_astro/screenshot.DL2AXHKQ_ZbXWYw.webp 768w, https://www.abahgat.com/_astro/screenshot.DL2AXHKQ_Z2vH72Y.webp 822w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 411px; max-height: 694px; aspect-ratio: 0.5922190201729106; width: 100%;&quot; alt=&quot;Reference UI screenshot accompanying the puzzle brief. I ended up using it as a spec for my client code.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Reference UI screenshot accompanying the puzzle brief. I ended up using it as a spec for my client code. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;h4 id=&quot;client&quot;&gt;Client&lt;/h4&gt;
&lt;p&gt;I started building this from the UI down.
The puzzle brief mentioned using &lt;code&gt;XMLHttpRequest&lt;/code&gt;, so I avoided using any client-side libraries (the functionality I was asked to build on the client was, after all, quite simple).
The screenshot included with the puzzle brief included just a text field for the search query and a list of results.&lt;/p&gt;
&lt;p&gt;I wrote a function to listen for key presses, dispatch an asynchronous call to the server and render the response as soon as it came back. By 2011, I had been coding web applications for a while and I was able to implement that functionality in less than an hour of work.&lt;/p&gt;
&lt;h4 id=&quot;web-application-and-servlet-code&quot;&gt;Web application and Servlet code&lt;/h4&gt;
&lt;p&gt;The Servlet layer was also quite simple, since all it had to was handle an incoming XML request and dispatch it to what the brief called a &lt;em&gt;database&lt;/em&gt;. Again, less than an hour of work here.&lt;/p&gt;
&lt;p&gt;At this level, I also wrote code to parse the database of strings to index from an XML file containing data from the National Register of Historic Places. The Tomcat server would run this code when loading my web application and use the resulting data to construct a data structure to use as an index for power the fast search functionality I needed to build. I needed to figure that out next.&lt;/p&gt;
&lt;h4 id=&quot;finding-a-suitable-data-structure&quot;&gt;Finding a suitable data structure&lt;/h4&gt;
&lt;p&gt;This is, unsurprisingly, the most challenging part of the puzzle and where I focused my efforts the most. As pointed out in the problem description, looping sequentially through the list of landmarks would not work (it would take much longer than the target 0.1ms threshold). I needed to find data structure with good runtime complexity associated with lookup operations.&lt;/p&gt;
&lt;p&gt;I spent some time thinking about how I would implement a data structure allowing the fast lookup times required in this case. The most common fast-lookup option I was familiar with, the &lt;em&gt;hash table&lt;/em&gt;, would not work straight away with this problem because it would expect the search operation to have the full key string.
In this problem, however, I wanted to be able to look up entries in my index even when given an incomplete substring, which would have required me to store all possible substrings as keys in the table.&lt;/p&gt;
&lt;p&gt;After doing some sketching on paper, it seemed reasonable to expect that &lt;a href=&quot;https://xlinux.nist.gov/dads/HTML/trie.html&quot;&gt;tries&lt;/a&gt; would work better here.&lt;/p&gt;
&lt;h4 id=&quot;suffix-trees&quot;&gt;Suffix trees&lt;/h4&gt;
&lt;p&gt;As I was researching data structures providing fast lookup operations given partial strings, I stumbled upon a number of papers referencing suffix trees, commonly used in computational biology and text processing, offering lookup operations with linear runtime with respect to the length of the string to search &lt;em&gt;for&lt;/em&gt; (as opposed to the length of the string to search &lt;em&gt;within&lt;/em&gt;).&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/cacao-st.DNFtSipy.svg&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;221&quot; height=&quot;240&quot; srcset=&quot;https://www.abahgat.com/_astro/cacao-st.DNFtSipy_Z1Yw6sR.svg 221w, https://www.abahgat.com/_astro/cacao-st.DNFtSipy_ErYNE.svg 400w, https://www.abahgat.com/_astro/cacao-st.DNFtSipy_1rMIv8.svg 442w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 221px; max-height: 240px; aspect-ratio: 0.9208333333333333; width: 100%;&quot; alt=&quot;Suffix Tree for the string `cacao`. A suffix is said to be contained in the tree if there is a path from the root node where the string obtained by concatenating the edge labels has the same prefix as the suffix being looked up. Highlighted the path corresponding to the `cao` suffix.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Suffix Tree for the string `cacao`. A suffix is said to be contained in the tree if there is a path from the root node where the string obtained by concatenating the edge labels has the same prefix as the suffix being looked up. Highlighted the path corresponding to the `cao` suffix. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;Plain suffix trees, however, are designed to find matches of a given candidate string sequence within a &lt;em&gt;single&lt;/em&gt;, longer, string, while this puzzle revolved around a slightly different use case: instead of having a single long string to look up matches in, I needed to be able to find matches in multiple strings. Thankfully, I read some more and found a good number of papers documenting data structures called &lt;a href=&quot;https://www.abahgat.com/project/suffix-tree&quot;&gt;&lt;em&gt;generalized&lt;/em&gt; suffix trees&lt;/a&gt; that do exactly that.&lt;/p&gt;
&lt;p&gt;Based on what I had learned so far, I was convinced this type of tree could fit my requirements but I had two likely challenges to overcome:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Suffix trees tend to occupy much more space than the strings they are indexing and, based on the problem statement, “the server’s JVM will be configured with 1024M maximum heap space” and that needed to accommodate the Tomcat server, my whole web application and the tree I was looking to build.&lt;/li&gt;
&lt;li&gt;Much of the complexity of working with suffix tree lies in &lt;em&gt;constructing&lt;/em&gt; the trees themselves. While the puzzle brief was explicitly saying my solution could take “several minutes to load”, I did not want the reviewer of my solution to have to wait several hours before they could test my submission.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ukkonens-algorithm-for-linear-runtime-tree-construction&quot;&gt;Ukkonen’s algorithm for linear runtime tree construction&lt;/h4&gt;
&lt;p&gt;Thankfully, had I found a popular algorithm for generating Suffix Trees in linear time (linear in the total length of the strings to be indexed), described by Ukkonen in a paper published in 1995 (&lt;a href=&quot;https://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf&quot;&gt;On–line construction of suffix trees&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;It took me a couple days of intermittent work (remember: I was working on this during nights and weekends — I had another day job back then) to get my suffix tree to work as expected.&lt;/p&gt;
&lt;p&gt;Interestingly, some of the challenges with this stage were revolving around a completely unexpected theme: Ukkonen’s paper includes the full algorithm written in pseudo-code and good prose detailing the core steps. However, that same pseudo-code is written at such a high level of abstraction that it did take some work to reconduct it to fast and efficient Java code.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;890&quot; height=&quot;514&quot; srcset=&quot;https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_Z1pgFT8.webp 400w, https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_19w43J.webp 768w, https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_ZbEqbH.webp 890w, https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_Z7zVcd.webp 1024w, https://www.abahgat.com/_astro/ukkonen-pseudocode.C7lPd-N0_Z1LBokq.webp 1780w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 890px; max-height: 514px; aspect-ratio: 1.7315175097276265; width: 100%;&quot; alt=&quot;Pseudo-code from Ukkonen&apos;s paper. While clear and easy to follow on the original paper, its translation to Java is much more verbose than this.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Pseudo-code from Ukkonen&apos;s paper. While clear and easy to follow on the original paper, its translation to Java is much more verbose than this. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;Also, the pseudo-code algorithm is written assuming we are working with a single string represented as a character array, so many of the operations outlined there deal with &lt;em&gt;indices&lt;/em&gt; within that large array (e.g. &lt;em&gt;k&lt;/em&gt; and &lt;em&gt;i&lt;/em&gt; in the procedure above).&lt;/p&gt;
&lt;p&gt;In my Java implementation, instead, I wanted to work with &lt;code&gt;String&lt;/code&gt; objects as much as possible. I was driven by a few different reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Java implements &lt;a href=&quot;https://en.wikipedia.org/wiki/String_interning&quot;&gt;string interning&lt;/a&gt; by default — there is no memory benefit in representing substrings by manually manipulating indices within an array of characters representing the containing string: the JVM &lt;em&gt;already does that&lt;/em&gt; transparently for us.&lt;/li&gt;
&lt;li&gt;Working with &lt;code&gt;String&lt;/code&gt; references led to code that was much more legible to me.&lt;/li&gt;
&lt;li&gt;I knew my next step would be to generalize the algorithm to handle building an index on &lt;em&gt;multiple&lt;/em&gt; strings and that was going to be much more difficult if I had to deal with low level specifics about which array of character represented which input string.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;generalized-suffix-trees&quot;&gt;&lt;em&gt;Generalized&lt;/em&gt; Suffix Trees&lt;/h4&gt;
&lt;p&gt;This last consideration proved to be critical: generalizing the suffix tree I had up to this point to work with multiple input strings was fairly straightforward. All I had to do was to make sure the nodes in my tree could carry some &lt;em&gt;payload&lt;/em&gt; denoting which of the strings in the index would match a given query string. This stage amounted to a couple hours of work, but only because I had good unit tests.&lt;/p&gt;
&lt;p&gt;At this point, things were looking great. I had spent maybe a couple days reading papers about suffix trees and another couple days writing all the code I had so far. I was ready to try out running my application with the input data provided with the puzzle brief: the entire National Register of Historic Places, an XML feed totaling a few hundred megabytes.&lt;/p&gt;
&lt;h4 id=&quot;trial-by-fire-outofmemoryerror&quot;&gt;Trial by fire: &lt;code&gt;OutOfMemoryError&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The first run of my application was disappointing. I started up Tomcat and deployed my web application archive, which triggered parsing the XML database provided as input and started to build the generalized suffix tree to use as an index for fast search. Not even two minutes into the suffix tree construction, the server crashed with an &lt;code&gt;OutOfMemoryError&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The 1024 megabytes I had were not enough.&lt;/p&gt;
&lt;p&gt;Thankfully, a couple years earlier I had worked with a client that had a difficult time keeping their e-commerce site up during peak holiday shopping season. Their servers kept crashing because they were running out of memory. That in turn led me to learn how to read and make sense of JVM memory dumps.&lt;/p&gt;
&lt;p&gt;I never thought I would make use of that skill for my own personal projects but this puzzle proved me wrong. I fired up &lt;a href=&quot;https://visualvm.github.io&quot;&gt;visualvm&lt;/a&gt; and started looking for the largest contributors to memory consumption.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/heapdump-classes-screen.ZqHPfiwz.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;600&quot; height=&quot;483&quot; srcset=&quot;https://www.abahgat.com/_astro/heapdump-classes-screen.ZqHPfiwz_Z7WpLa.webp 400w, https://www.abahgat.com/_astro/heapdump-classes-screen.ZqHPfiwz_2kDq5z.webp 600w, https://www.abahgat.com/_astro/heapdump-classes-screen.ZqHPfiwz_ZgB3ME.webp 768w, https://www.abahgat.com/_astro/heapdump-classes-screen.ZqHPfiwz_2c5YK0.webp 1024w, https://www.abahgat.com/_astro/heapdump-classes-screen.ZqHPfiwz_Z1GmVzP.webp 1200w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 600px; max-height: 483px; aspect-ratio: 1.2422360248447204; width: 100%;&quot; alt=&quot;A screenshot of VisualVM used to inspect a heap dump (from the official documentation)&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; A screenshot of VisualVM used to inspect a heap dump (from the official documentation) &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;It did not take long to find that there were a few memory allocation patterns that were not efficient. Many of these items would hardly be an issue for an average application, but they all ended up making a difference in this case because of the sheer size of the tree data structure being constructed.&lt;/p&gt;
&lt;h4 id=&quot;memory-micro-optimizations&quot;&gt;Memory micro-optimizations&lt;/h4&gt;
&lt;p&gt;Analyzing a few heap dumps suggested me a series of possible changes that would lead to savings in memory, usually at the cost of additional complexity or switching from a general purpose data structure implementation (e.g. maps) to special purpose equivalent tailored to this use case and its constraints.&lt;/p&gt;
&lt;p&gt;I ranked possible optimizations by their expected return on investment (i.e. comparing value of the memory savings to the additional implementation complexity, slower runtime and other factors) and implemented a few items at the top of the list.&lt;/p&gt;
&lt;p&gt;The most impactful changes involved optimizing the memory footprint of the suffix tree &lt;em&gt;nodes&lt;/em&gt;: considering my application required constructing a very large graph (featuring tens of thousands of nodes), any marginal savings coming from a more efficient node representation would end up making a meaningful difference.&lt;/p&gt;
&lt;p&gt;A property of suffix tree nodes is that no outgoing edges can be labeled with strings sharing a prefix. In practice, this means that the data structure implementing a node must hold a reference to a set of outgoing edges keyed by the first character on the label.&lt;/p&gt;
&lt;p&gt;The first version of my solution was using a &lt;code&gt;HashMap&amp;lt;Character,Edge&amp;gt;&lt;/code&gt; to represent this. As soon as I looked at the heap dump, I noticed this representation was extremely inefficient for my use case.&lt;/p&gt;
&lt;p&gt;Hash Maps in Java are initialized with a &lt;a href=&quot;https://en.wikipedia.org/wiki/Hash_table#Key_statistics&quot;&gt;load factor&lt;/a&gt; of 0.75 (meaning they generally reserve memory for at least 25% more key/value pairs than they hold at any given point) and, more importantly, with enough initial capacity to hold 16 elements.&lt;/p&gt;
&lt;p&gt;The latter item was a particularly poor fit for my use case: since I was indexing strings using the English alphabet (26 distinct characters) a map of size 16 would be large enough to accommodate more than half the possible characters and would often be wasteful.&lt;/p&gt;
&lt;p&gt;I could have mitigated this problem by tuning the sizing and load factor parameters but I thought I could save even more memory by switching to a specialized collection type.
The default map implementations included in the standard library require the key and value types to be reference types rather than native types (i.e. the map is keyed by &lt;code&gt;Character&lt;/code&gt; instead of &lt;code&gt;char&lt;/code&gt;) and reference types tend to be much less memory efficient (since their representation is more complex).&lt;/p&gt;
&lt;p&gt;I wrote a special-purpose map implementation, called &lt;code&gt;EdgeBag&lt;/code&gt;, which featured a few tweaks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stored keys and values and two parallel arrays,&lt;/li&gt;
&lt;li&gt;the arrays would start small gradually grew if more space if necessary,&lt;/li&gt;
&lt;li&gt;relied on a linear scan for lookup operation if the bag contained a small number of elements and switched to using binary search on a sorted key set if the bag had grown to contain more than a few units,&lt;/li&gt;
&lt;li&gt;used &lt;code&gt;byte[]&lt;/code&gt; (instead of &lt;code&gt;char[]&lt;/code&gt;) to represent the characters in the keys. Java’s 16-bit &lt;code&gt;char&lt;/code&gt; type takes twice as much space as a &lt;code&gt;byte&lt;/code&gt;. I knew all my keys were ASCII characters, so I could forgo Unicode support here and could squeeze some more savings by casting to a more narrow value range.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some more specific details on this and other changes to reduce the memory footprint of my suffix tree implementation are in the &lt;a href=&quot;https://www.abahgat.com/project/suffix-tree#problem-specific-optimizations&quot;&gt;Problem-specific optimizations&lt;/a&gt; section of the Suffix Tree project page.&lt;/p&gt;
&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;When I tested out my program after the memory optimizations, I was delighted to see it met the problem requirements: lookups were lightning fast, well under 0.1ms using the machine I had back then (based on an Intel Q6600 2.4GHz CPU) and the unit tests I had written gave me good confidence that the program behaved as required.&lt;/p&gt;
&lt;p&gt;I packaged up the solution as a WAR archive, wrote a brief README file outlining design considerations and instructions on how to run it (just deploy on a bare Tomcat 6 server) and sent it over email. Almost a year later, I was packing my bags and moving to Amsterdam to join Google (which had by then acquired ITA Software).&lt;/p&gt;
&lt;p&gt;I owe it in no small part to the fun I had with this coding puzzle.&lt;/p&gt;
&lt;p&gt;When I think of how much I enjoyed the time I spent building Instant Search, I think it must be because it required both breadth (to design a full stack application, albeit a simple one) and depth (to research the best data structure for the job and follow up with optimizations as required). It allowed me to combine my background as a generalist with my interest with the theoretical foundations of Computer Science.&lt;/p&gt;
&lt;p&gt;The careful choice of specifying both memory and runtime constraints as part of the problem requirements made the challenge much more fun. When the first version I coded did not work, I was able to reuse my experience with memory profiling tools to identify which optimizations to follow up with. At the same time, I built a stronger understanding of Java’s internals and learned a lot more about implementation details I had, until then, just given for granted.&lt;/p&gt;
&lt;p&gt;When ITA retired Instant Search (and other programming puzzles&lt;sup&gt;&lt;a href=&quot;#user-content-fn-puzzles&quot; id=&quot;user-content-fnref-puzzles&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;), I decided to &lt;a href=&quot;https://www.abahgat.com/project/suffix-tree&quot;&gt;release the Java Generalized Suffix Tree as open source&lt;/a&gt; for others to use. Despite the many problem-specific optimizations I ended up making, it is generic enough that has been used in a few other applications since I built it, which gives me one more thing to be thankful for.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-puzzles&quot;&gt;
&lt;p&gt;While the original page is no longer online, the Wayback Machine still has a snapshot of the original page with the original selection of &lt;a href=&quot;https://web.archive.org/web/20111012115624/http://itasoftware.com/careers/work-at-ita/hiring-puzzles.html&quot;&gt;past programming puzzles&lt;/a&gt;. They are still a great way to test your programming skills. &lt;a href=&quot;#user-content-fnref-puzzles&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>What to look for when hiring</title><link>https://www.abahgat.com/blog/what-to-look-for-when-hiring</link><guid isPermaLink="true">https://www.abahgat.com/blog/what-to-look-for-when-hiring</guid><description>A while ago, I found myself in the enviable position of having to rapidly grow my team. Here a list of the most important characteristics I learned to value in anyone I work with, regardless of job function.</description><pubDate>Mon, 26 Aug 2019 19:09:10 GMT</pubDate><content:encoded>&lt;p&gt;A while ago, I found myself in the enviable position of having to rapidly grow my team. By then, I had done a large number of technical interviews, so I had an idea of what to look for in strong candidates for Software Engineering positions. However, I felt like I lacked a framework for understanding how likely a given candidate was to succeed if they had joined my team, beyond a very loose definition of “culture fit”.&lt;/p&gt;
&lt;p&gt;As I was trying to better understand what I was looking for, I started to think about what I value in the people I work with and to reflect on traits I found to be quite common among some of the most successful people I have worked with over the course of my career.&lt;/p&gt;
&lt;p&gt;While I would not expect every person I work with to exhibit &lt;em&gt;all&lt;/em&gt; the qualities I list here, I am always positively impressed when I come across someone who exhibits more than a few and equally concerned when I see no hint of any of these characteristics.&lt;/p&gt;
&lt;p&gt;Over time, I became quite sensitive to some hints that suggest someone could possess one of the these traits and I learned to probe further whenever I see them.&lt;/p&gt;
&lt;p&gt;Here a list of the most important characteristics I learned to value in anyone I work with, regardless of job function.&lt;/p&gt;
&lt;h2 id=&quot;intrinsic-motivation&quot;&gt;Intrinsic Motivation&lt;/h2&gt;
&lt;p&gt;Many of the best people I worked with are motivated by their own desire to improve, regardless of the environment around them. Certainly, having a great team and a lot of attention from their manager will help them as well as it would help anyone else, but being intrinsically motivated means they are able to find satisfaction without relying on artificial nudges from the system around them.&lt;/p&gt;
&lt;p&gt;I tend to enjoy working with people who think this way because they are often pushing themselves to get better every day, react better to difficulties and challenges and, as a result, push me to get better as well.&lt;/p&gt;
&lt;p&gt;I know I am looking at someone who has this kind of attitude when they show they are driven by things such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;learning something new every day&lt;/li&gt;
&lt;li&gt;mastering a skill or a craft&lt;/li&gt;
&lt;li&gt;accomplishing something they thought of as difficult&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having hobbies and non-trivial side-projects (for those of us who are at a point where they can afford the time required) is often a sign of being intrinsically motivated.&lt;/p&gt;
&lt;h2 id=&quot;relentless-focus&quot;&gt;Relentless Focus&lt;/h2&gt;
&lt;p&gt;Success often requires from focusing on the most important things first and almost ignoring everything else.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Effective executives concentrate on the few major areas where superior performance will produce outstanding results. They force themselves to set priorities and stay with their priority decisions. They know that they have no choice but to do first things first—and second things not at all. The alternative is to get nothing done. &lt;span class=&quot;blockquote-footer&quot;&gt;Peter F. Drucker, &lt;a href=&quot;https://amzn.to/2ZndZml&quot;&gt;&lt;cite title=&quot;The Effective Executive&quot;&gt;The Effective Executive&lt;/cite&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I found it hard to gauge how good anyone is at focusing on top priorities based solely on casual conversations. One decent proxy, at least for technical roles, are open ended system design interviews. Many good questions involve asking to solve a problem too large to be tackled within the allotted time or with the given constraints. That forces the candidate to narrow down the scope and focus on the most important aspects of the problem and set everything else aside.&lt;/p&gt;
&lt;h2 id=&quot;independent-thinking&quot;&gt;Independent Thinking&lt;/h2&gt;
&lt;p&gt;A couple of the best people I worked with have a way of asking questions that sometimes can come across as blunt or excessively direct. In their case, I have never had a problem with it, since it is tied to what I believe to be one of their strengths: they are not afraid to question a line of thought if they do not fully understand it or if they disagree with it.&lt;/p&gt;
&lt;p&gt;In cultures where it is more comfortable to agree with others than to challenge their thinking, it takes courage to express dissent.&lt;/p&gt;
&lt;p&gt;I &lt;a href=&quot;https://www.abahgat.com/post/2012-03-12-building-a-culture-of-objection/index.md&quot;&gt;wrote before&lt;/a&gt; how much I value a culture where anyone feels free to voice their disagreement: I value even more individuals who are comfortable speaking up regardless of what the environment surrounding them looks like.&lt;/p&gt;
&lt;p&gt;This is another trait that is be hard to spot in casual conversations, I have seen this come across as a set of pointed, specific questions aimed at developing a stronger understanding of a topic and then thoughtfully suggesting there might be a different way to approach a problem.&lt;/p&gt;
&lt;p&gt;However, there is a fine line between being willing to challenge ideas when they are not rock solid and being contrarian by default: it is hard to work with someone who disagrees with everything on principle.&lt;/p&gt;
&lt;h2 id=&quot;fast-learning&quot;&gt;Fast Learning&lt;/h2&gt;
&lt;p&gt;The ability to learn quickly and adapt to changing circumstances is one of the most critical skills to have in this day and age. To me, it means that I can trust someone to be able to be asked to do something they have not done before and rapidly get up to speed.&lt;/p&gt;
&lt;p&gt;I generally see this through evidence of high rate of improvement; whether it shows as gaining mastery of many technologies in a short time, working across a number of different domains or being promoted repeatedly while at the same company, this shows an ability to adapt to changing circumstances.&lt;/p&gt;
&lt;h2 id=&quot;responsiveness-and-follow-through&quot;&gt;Responsiveness and Follow Through&lt;/h2&gt;
&lt;p&gt;One of the main differences between working with a team and working by ourselves is that when we are part of a team others tend to depend on our output for their own progress.&lt;/p&gt;
&lt;p&gt;Oftentimes, managers end being stuck having to play the role of the persistent nag, reminding others of their prior commitments and making sure that any work that was agreed upon is eventually delivered. Clearly, this is a way around a fairly common problem: the average person is not great at following through.&lt;/p&gt;
&lt;p&gt;By contrast, the most effective team players I have worked with hardly need any nudges: they will stay on top of their to-do list and consistently deliver anything they agreed to do by the time they said they would, without you ever needing to ask again.
If you do ask something of them, they respond right away.&lt;/p&gt;
&lt;p&gt;Sadly, I do not know of a way to assess how well anyone would do on this point without speaking to anyone who has worked with them before.&lt;/p&gt;
&lt;h2 id=&quot;decisiveness&quot;&gt;Decisiveness&lt;/h2&gt;
&lt;p&gt;Many people struggle with decisions, for fear of making a mistake, being proven wrong and fallible or committing to the wrong direction. Whatever the reason, shying away from decisions is rarely helpful.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“In effect, the lack of a decision is the same as a negative decision; no green light is a red light, and work can stop for a whole organization.” &lt;span class=&quot;blockquote-footer&quot;&gt;Andrew S. Grove, &lt;a href=&quot;https://amzn.to/32dlLB9&quot;&gt;&lt;cite title=&quot;High Output Management&quot;&gt;High Output Management&lt;/cite&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The truth is that many decisions are relatively easy to reverse if necessary but the cost of paralysis is too high for most teams and organizations to afford. High-stakes decisions are rare, but when facing one it is important to treat it as a priority and not linger too long. The worst thing we can do is simply dwell on it and get stuck.&lt;/p&gt;
&lt;p&gt;Decisiveness is often the driving force behind the &lt;a href=&quot;#responsiveness-and-follow-through&quot;&gt;responsiveness&lt;/a&gt; in the previous section.&lt;/p&gt;
&lt;h2 id=&quot;curiosity-and-inquisitiveness&quot;&gt;Curiosity and Inquisitiveness&lt;/h2&gt;
&lt;p&gt;Beyond being a &lt;a href=&quot;#fast-learning&quot;&gt;fast learner&lt;/a&gt; or being passionate about the specifics of someone’s own job, being curious and inquisitive can be invaluable in understanding one’s own teammates, manager, users and competitors.&lt;/p&gt;
&lt;p&gt;By wondering about the “why” behind anything we observe, we develop a stronger understanding of the problem we are trying to solve or the parties and organizations we are working with. An understanding that inevitably helps us be more effective.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“When you get curious and learn how to turn that disagreement into honest questioning, you can learn more about other perspectives on the issue because your team will open up.”
&lt;span class=&quot;blockquote-footer&quot;&gt;Camille Fournier, &lt;a href=&quot;https://amzn.to/2KY97jH&quot;&gt;&lt;cite title=&quot;The Manager&apos;s Path&quot;&gt;The Manager’s Path&lt;/cite&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Oddly enough, at least based on my own experience, it is fairly common to find engineers who are extremely curious about technical topics but tend to be less interested about understanding less technical subjects (such as organizations and other humans). People I worked with who are truly inquisitive tend to demonstrate it by being uncommonly interested in the motivation behind the status quo or previous decisions. They often ask questions such as “Why do we do things this way?”&lt;/p&gt;
&lt;h2 id=&quot;communication&quot;&gt;Communication&lt;/h2&gt;
&lt;p&gt;So much of teamwork is communication, yet communication skills are often overlooked. It is hard to overstate the importance of communication in teamwork. Effective communication means, among other things,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;being able to make one’s point of view understood,&lt;/li&gt;
&lt;li&gt;resolving conflicts,&lt;/li&gt;
&lt;li&gt;selling our own vision,&lt;/li&gt;
&lt;li&gt;making sure others are aware of our work (and why it matters)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of all the traits I learned to appreciate, this is perhaps the most visible. If you spend even a few minutes speaking with someone and they are an effective communicator, you will notice.&lt;/p&gt;
&lt;h2 id=&quot;going-the-extra-mile&quot;&gt;Going the Extra Mile&lt;/h2&gt;
&lt;p&gt;Many successful people consistently overdeliver. It is quite difficult to have any sort of success by just doing the bare minimum. Sure, one can get lucky once or twice, but solid careers are built on strings of consistent achievements.&lt;/p&gt;
&lt;p&gt;I often see this in coming through from people’s passions. It often shows as side projects (work-like activities they chose to do in their own time&lt;sup&gt;&lt;a href=&quot;#user-content-fn-side-projects&quot; id=&quot;user-content-fnref-side-projects&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;) or initiatives at work that they started without anyone asking them to do so (e.g. 20% projects at Google).&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-side-projects&quot;&gt;
&lt;p&gt;Note that this is not always possible for people to do, depending on their situation. &lt;a href=&quot;#user-content-fnref-side-projects&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Visual and HTML Testing for Static Sites</title><link>https://www.abahgat.com/blog/testing-static-sites</link><guid isPermaLink="true">https://www.abahgat.com/blog/testing-static-sites</guid><description>I set up a CI/CD pipeline to test my website for markup and rendering issues. It proved to be so useful that I can not imagine going back.</description><pubDate>Tue, 06 Aug 2019 11:06:27 GMT</pubDate><content:encoded>&lt;p&gt;Over a year ago I switched from having my site hosted on a CMS to having it &lt;a href=&quot;https://www.abahgat.com/blog/2018-03-14-migrating-from-wordpress-to-hugo&quot; title=&quot;Migrating from Wordpress to Hugo.&quot;&gt;built statically&lt;/a&gt; and served as a collection of static pages. I have been extremely happy with the end result for all these months — the site is very easy to update and effortless to maintain — but I just made a few changes that made my experience even better.&lt;/p&gt;

&lt;h2 id=&quot;why-test-static-sites&quot;&gt;Why test Static Sites&lt;/h2&gt;
&lt;p&gt;Even for sites as simple as this, it is surprisingly easy to make breaking changes without realizing. Over the time I have been maintaining abahgat.com, I ended up accidentally introduction bugs more than a few times. Here a few examples of things I ran into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;broken links&lt;/em&gt; — by default, Hugo does not validate any of the links in the content I am editing, which means that I have to be careful and make sure all URLs and paths are valid&lt;/li&gt;
&lt;li&gt;&lt;em&gt;incorrect theme configuration&lt;/em&gt; — the more complex the theme I am using is, the more configuration options it will offer. The more options I have to configure, the more likely I am to make mistakes.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;bugs in theme customizations&lt;/em&gt; — Hugo is great at allowing to override and customize theme templates. However, this is another source of potential issues.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;bugs in the theme code itself&lt;/em&gt; — No software is perfect, and any theme I might be using can have its own bugs and edge cases. This might be especially true for you if you are actively developing your own theme or you frequently update it to the most recent version available.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of the issues above still affected me when I was hosting my site on Wordpress (I did break links and styling every now and then) but one advantage of working with a statically generated site is that we can leverage many of the tools that are available to web developers to catch issues early (and potentially block deploys if any issues are detected). So I set out to find what kind of options I had to improve my workflow so that I could make changes with more confidence that I wouldn’t accidentally break my site.&lt;/p&gt;
&lt;h2 id=&quot;what-can-be-tested&quot;&gt;What can be tested&lt;/h2&gt;
&lt;p&gt;Based on the list above, I knew I was looking to set up tests to detect, in order of priority, problems such as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;broken internal links&lt;/li&gt;
&lt;li&gt;invalid or malformed HTML&lt;/li&gt;
&lt;li&gt;issues with layout or presentation&lt;/li&gt;
&lt;li&gt;invalid RSS feed entries&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Thankfully, I was able to find a way to cover most of these.&lt;/p&gt;
&lt;h3 id=&quot;testing-html-with-html-proofer&quot;&gt;Testing HTML with &lt;code&gt;html-proofer&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Covering the first items on the list has been fairly straightforward with &lt;a href=&quot;https://github.com/gjtorikian/html-proofer&quot;&gt;&lt;code&gt;html-proofer&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Provided you have Ruby installed, you can get &lt;code&gt;html-proofer&lt;/code&gt; as a gem via the command below&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gem&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; html-proofer&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then run it via&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;htmlproofer&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --extension&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; .html&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./public&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will scan the &lt;code&gt;./public&lt;/code&gt; directory for any files with &lt;code&gt;html&lt;/code&gt; extension and output a report listing any issues with the markup in those files.&lt;/p&gt;
&lt;p&gt;When I first ran it on my site, I got a pretty good list of actionable warnings. The messages are fairly specific and easy to understand, as you can tell by looking at the snippet below:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- ./public/author/abahgat/index.html&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  356:11: ERROR: Opening and ending tag mismatch: section and div (line 356)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- ./public/author/index.html&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  356:11: ERROR: Opening and ending tag mismatch: section and div (line 356)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- ./public/blog/index.html&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  829:2157: ERROR: Unexpected end tag : p (line 829)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- ./public/blog/maps-for-public-transport-users/index.html&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  internally linking to uploads/2009/01/p-480-320-0e6ac38d-252e-47fa-be79-0ae974dad8d2.jpeg, which does not exist (line 476)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     &amp;lt;a href=&quot;uploads/2009/01/p-480-320-0e6ac38d-252e-47fa-be79-0ae974dad8d2.jpeg&quot;&amp;gt;&amp;lt;img class=&quot;size-full wp-image-364 aligncenter&quot; src=&quot;https://www.abahgat.com/img/wp-uploads/2009/01/p-480-320-0e6ac38d-252e-47fa-be79-0ae974dad8d2.jpeg&quot; alt=&quot;&quot; width=&quot;200&quot; height=&quot;300&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- ./public/blog/page/2/index.html&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  linking to internal hash #broken-priorites that does not exist (line 1456)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     &amp;lt;a href=&quot;#broken-priorites&quot;&amp;gt;The way priorities are managed is broken&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  linking to internal hash #duplicates that does not exist (line 1453)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     &amp;lt;a href=&quot;#duplicates&quot;&amp;gt;Lots of issues are duplicates&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  linking to internal hash #missing-info that does not exist (line 1455)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     &amp;lt;a href=&quot;#missing-info&quot;&amp;gt;Bug reports do not include enough information&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  linking to internal hash #processes that does not exist (line 1454)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     &amp;lt;a href=&quot;#processes&quot;&amp;gt;The system imposes over-engineered processes&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  *  linking to internal hash #tracker-misuse that does not exist (line 1452)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     &amp;lt;a href=&quot;#tracker-misuse&quot;&amp;gt;The issue tracking system is misused&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even with default settings, &lt;code&gt;html-proofer&lt;/code&gt; is able to catch most of the issues I was interested in detecting: the list above features a good mix of problems caused by invalid links in my Markdown sources, errors due to how I was misusing my template and bugs in the template I was using.&lt;/p&gt;
&lt;p&gt;Fixing the issues required a combination of updating a few broken links, cleaning up the Markdown sources for my site, submitting a few bugs and Pull Requests against the theme I am using.&lt;/p&gt;
&lt;p&gt;Overall, all the issues flagged made sense and worth fixing.&lt;/p&gt;
&lt;h3 id=&quot;visual-testing-with-percy&quot;&gt;Visual Testing with Percy&lt;/h3&gt;
&lt;p&gt;As useful as &lt;code&gt;html-proofer&lt;/code&gt; is, it does not help catching layout and presentational issues that are not due to invalid markup. I have had good experiences with visual testing and review at work and I was interested in using screenshots to detect layout issues and catch any unintended presentational changes on my own site too.&lt;/p&gt;
&lt;p&gt;I cared about this because upgrading my Hugo theme sometimes involves non-trivial changes that could go wrong (despite George, the author, keeping &lt;a href=&quot;https://sourcethemes.com/academic/updates/v4.5.0/&quot;&gt;really good change logs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Also, I wanted to make customizations to the theme and having testing in place is the only way I know to make sure I don’t inadvertently break anything (since I will not review every single page manually every time I make layout changes, having a way to be warned about any differences is very valuable).&lt;/p&gt;
&lt;p&gt;I ended up settling on &lt;a href=&quot;https://www.percy.io&quot;&gt;Percy&lt;/a&gt;, a tool that was clearly designed first and foremost for testing dynamic web applications but also offered an option to &lt;a href=&quot;https://docs.percy.io/docs/static-sites&quot;&gt;test static sites&lt;/a&gt; via a command line program.&lt;/p&gt;
&lt;p&gt;The main idea behind a snapshot testing system is to keep a set of approved snapshots (“goldens”), capture a new set of snapshots upon change and flag any differences for review. Changes can be either intended (in which case the screenshot is approved and becomes the new golden) or accidental (in which case they are flagged as regressions and expected to be fixed before pushing a new version).&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/diff-categories.CIjbA6Go.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;2235&quot; height=&quot;741&quot; srcset=&quot;https://www.abahgat.com/_astro/diff-categories.CIjbA6Go_29Y56G.webp 400w, https://www.abahgat.com/_astro/diff-categories.CIjbA6Go_1NlLLV.webp 768w, https://www.abahgat.com/_astro/diff-categories.CIjbA6Go_sJpgU.webp 1024w, https://www.abahgat.com/_astro/diff-categories.CIjbA6Go_EldWH.webp 2040w, https://www.abahgat.com/_astro/diff-categories.CIjbA6Go_iR9z9.webp 2235w, https://www.abahgat.com/_astro/diff-categories.CIjbA6Go_Z2iRhFz.webp 4470w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 2235px; max-height: 741px; aspect-ratio: 3.016194331983806; width: 100%;&quot; alt=&quot;Example screenshot highlighting differences introduced by a specific commit.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Example screenshot highlighting differences introduced by a specific commit. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;Percy offers a nice interface to highlight any difference between snapshots and can be easily integrated with GitHub and other source control systems to make approving any updated snapshots part of the code review process.&lt;/p&gt;
&lt;p&gt;Percy runs as a service, so you will need to create an account with them before being able to use it. Once you have done that you can try it by following the instructions on &lt;a href=&quot;https://docs.percy.io/docs/command-line-client&quot;&gt;their documentation page&lt;/a&gt; and running the following command on your site (where &lt;code&gt;./public&lt;/code&gt; is a directory containing your static pages):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; percy&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; snapshot&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./public&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;running-tests-on-every-change-via-ci-services&quot;&gt;Running tests on every change via CI services&lt;/h2&gt;
&lt;p&gt;Unlike the HTML tests, which test a specific version of your site in isolation, the value of snapshot testing lies in comparing your site against a previously approved set of snapshots, which need to be kept up to date.&lt;/p&gt;
&lt;p&gt;I then configured a simple workflow with &lt;a href=&quot;https://circleci.com/&quot;&gt;CircleCI&lt;/a&gt;, having it build my site with Hugo, run &lt;code&gt;html-proofer&lt;/code&gt; on the generated sources, grab a fresh set of screenshots on every change and flag any differences for review.&lt;/p&gt;
&lt;p&gt;From what I could tell, many other CI services can be configured to do the same; I ended up choosing CircleCI because I thought its Docker-based setup worked better for what I was trying to do and I had little trouble finding Docker images suitable for running the steps in my workflow.&lt;/p&gt;
&lt;p&gt;Below the resulting configuration:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;orbs&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  hugo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;circleci/hugo@0.3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  snapshot&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    docker&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;image&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;buildkite/puppeteer:v1.15.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;attach_workspace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          at&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;npm install percy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;PERCY_TOKEN=$PERCY_TOKEN npx percy snapshot ./public&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;workflows&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    jobs&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;hugo/build&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;0.55.6&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          html-proofer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;snapshot&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          requires&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;hugo/build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first section sets up build with Hugo via an &lt;em&gt;Orb&lt;/em&gt; (&lt;a href=&quot;https://circleci.com/orbs/&quot;&gt;Orbs&lt;/a&gt; are CircleCI’s packages of functionality that can be packaged and reused) that also runs &lt;code&gt;html-proofer&lt;/code&gt; tests on the resulting build.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;snapshot&lt;/code&gt; task installs &lt;code&gt;percy&lt;/code&gt; via npm and then invokes it on the directory containing the sources generated in the previous step. It runs on the &lt;a href=&quot;https://hub.docker.com/r/buildkite/puppeteer&quot;&gt;Docker Puppeteer&lt;/a&gt; image, which comes with most of Percy’s package dependencies already installed.&lt;/p&gt;
&lt;div class=&quot;not-prose flex gap-3 p-4 rounded-lg border bg-surface border-border-default text-default  alert alert-note&quot;&gt; &lt;svg width=&quot;1em&quot; height=&quot;1em&quot; class=&quot;w-5 h-5 flex-shrink-0 mt-2&quot; data-icon=&quot;tabler:info-circle&quot;&gt;   &lt;symbol id=&quot;ai:tabler:info-circle&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;path d=&quot;M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0m9-3h.01&quot;/&gt;&lt;path d=&quot;M11 12h1v4h1&quot;/&gt;&lt;/g&gt;&lt;/symbol&gt;&lt;use href=&quot;#ai:tabler:info-circle&quot;&gt;&lt;/use&gt;  &lt;/svg&gt; &lt;div&gt;  &lt;div class=&quot;text-sm prose-p:my-0 prose-p:first:mt-0 prose-p:last:mb-0 [&amp;amp;&gt;p]:my-0&quot;&gt; &lt;p&gt;There seems to be a &lt;a href=&quot;https://hub.docker.com/r/percyio/agent&quot;&gt;Docker image maintained by Percy&lt;/a&gt; but I could not get it
to work. I suspect it is because it ships with an old version of the &lt;code&gt;percy&lt;/code&gt; command, I did not investigate this
further.&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt;
&lt;p&gt;With this configuration, every commit and Pull Request will trigger a Hugo build, run your site through &lt;code&gt;html-proofer&lt;/code&gt; and capture a new set of snapshots. If any visual differences are detected, they can be inspected and approved via Percy’s web interface.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/pr-with-checks.qoXq374x.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1359&quot; height=&quot;704&quot; srcset=&quot;https://www.abahgat.com/_astro/pr-with-checks.qoXq374x_Z1NtvC7.webp 400w, https://www.abahgat.com/_astro/pr-with-checks.qoXq374x_15dUCV.webp 768w, https://www.abahgat.com/_astro/pr-with-checks.qoXq374x_Z1etCH8.webp 1024w, https://www.abahgat.com/_astro/pr-with-checks.qoXq374x_13KOV8.webp 1359w, https://www.abahgat.com/_astro/pr-with-checks.qoXq374x_nEVrL.webp 2040w, https://www.abahgat.com/_astro/pr-with-checks.qoXq374x_Z1xNOU8.webp 2718w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1359px; max-height: 704px; aspect-ratio: 1.9303977272727273; width: 100%;&quot; alt=&quot;GitHub will show the latest status of your tests on every commit and Pull Request.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; GitHub will show the latest status of your tests on every commit and Pull Request. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;Note that there is no &lt;em&gt;deploy&lt;/em&gt; workflow since I configured Netlify to automatically publish a new version of my site whenever I push to the &lt;em&gt;master&lt;/em&gt; branch.&lt;/p&gt;
&lt;h2 id=&quot;tweaking-the-setup&quot;&gt;Tweaking the setup&lt;/h2&gt;
&lt;p&gt;If you got to this point, your configuration will feature sensible defaults and help you capture a number of issues caused by your own mistakes or any issues introduced by the theme upstream.&lt;/p&gt;
&lt;p&gt;There are a few opportunities to make the setup more efficient, but they require making changes with the CircleCI configuration above since the Orb we used before does not expose a good way to pass flags to tweak neither the build nor test test. (This &lt;a href=&quot;https://github.com/CircleCI-Public/hugo-orb/issues?q=author%3Aabahgat&quot;&gt;might be fixed&lt;/a&gt; by the time you read this).&lt;/p&gt;
&lt;p&gt;You can &lt;a href=&quot;https://gist.github.com/abahgat/e7fc5b3023692610c4760fedcd8e3b43&quot;&gt;click here&lt;/a&gt; to see a CircleCI configuration file that you can further customize based on the sections below.&lt;/p&gt;
&lt;p&gt;Here some of the tweaks you might consider implementing.&lt;/p&gt;
&lt;h3 id=&quot;test-pages-with-a-future-publish-date-and-drafts&quot;&gt;Test pages with a future publish date and drafts&lt;/h3&gt;
&lt;p&gt;Hugo allows you mark pages as drafts or to set a publish date to a future time (for scheduled content). Neither of these pages will be built by default in your deploy workflow, but you might want to do that when running your tests so that you ensure that content passes validation even as it is being edited (as opposed to being surprised by unexpected errors just when you thought you were ready to publish).&lt;/p&gt;
&lt;p&gt;You can do this by passing the &lt;code&gt;-D&lt;/code&gt; and &lt;code&gt;-F&lt;/code&gt; flags to the &lt;code&gt;hugo&lt;/code&gt; command during the build step.&lt;/p&gt;
&lt;h3 id=&quot;consider-enabling-minification&quot;&gt;Consider enabling minification&lt;/h3&gt;
&lt;p&gt;If you are building your site with minification enabled when you are deploying, you might have to make a decision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;if you enable minification only on the deploy workflow (and leave it disabled for development), the version of the site you will be testing will not be identical to the version you are publishing. This might hide subtle bugs that you would not be able to track down easily (such as &lt;a href=&quot;https://github.com/gcushen/hugo-academic/issues/1219&quot;&gt;this one&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;on the contrary, if you do enable minification, debugging issues flagged by &lt;code&gt;html-proofer&lt;/code&gt; and &lt;code&gt;percy&lt;/code&gt; might be slightly more difficult, since the resulting source code will be more difficult to read.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I do not have a firm recommendation here, I am currently working with the latter setup and it has been working fine so far but isolating the cause of an issue is slightly harder this way.&lt;/p&gt;
&lt;p&gt;If you want try this, you need to pass &lt;code&gt;--minify&lt;/code&gt; to the &lt;code&gt;hugo&lt;/code&gt; command during the build step.&lt;/p&gt;
&lt;h3 id=&quot;skip-redundant-screenshots&quot;&gt;Skip redundant screenshots&lt;/h3&gt;
&lt;p&gt;Just like, when writing unit tests, we don’t want to have multiple redundant tests that cover the same behavior, in most cases it is not necessary to take screenshots of pages that use the same template and have very similar content.&lt;/p&gt;
&lt;p&gt;For example, if part of your site is a blog that features tags and categories (in Hugo, this would apply to any &lt;a href=&quot;https://gohugo.io/content-management/taxonomies/&quot;&gt;&lt;em&gt;taxonomy&lt;/em&gt;&lt;/a&gt;), you will not need to take screenshot of every individual tag page as you won’t get much value out of them, since they all look the same. They will rather be a burden to maintain (should your theme ever change, you’d have many more — very similar — screenshots to approve).&lt;/p&gt;
&lt;p&gt;You can probably make a similar case for directory pages (say, if you have 40 pages of articles, the screenshots for the second to thirty-ninth pages are likely going to be the same.
There could be value in testing the first and last page separately since you’d imagine they would have a different configuration for the next/previous navigation elements, but that is up to you.&lt;/p&gt;
&lt;p&gt;Thankfully, the &lt;code&gt;percy&lt;/code&gt; command offers a way to manually exclude certain paths from being considered when grabbing screenshots. The syntax for that argument expects &lt;a href=&quot;https://en.wikipedia.org/wiki/Glob_(programming)&quot;&gt;globs&lt;/a&gt;, which can take some trial and error to get right.&lt;/p&gt;
&lt;p&gt;In case it helps, here a configuration that worked reasonably well for me so far:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; percy&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; snapshot&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./public&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -i&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;  &apos;categories/!(coding|coding/**)/*.html&apos;,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;  &apos;tags/!(amsterdam|amsterdam/**)/*.html&apos;,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;  &apos;blog/page/!(1|2)/*.html&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What the above is doing is excluding all categories but one (&lt;a href=&quot;https://www.abahgat.com/categories/coding&quot;&gt;Coding&lt;/a&gt;) and all tags excluding one (&lt;a href=&quot;https://www.abahgat.com/tags/amsterdam&quot;&gt;Amsterdam&lt;/a&gt;). It is also ignoring any page beyond the second in the &lt;code&gt;/blog&lt;/code&gt; directory.&lt;/p&gt;
&lt;h3 id=&quot;capture-screenshots-less-frequently&quot;&gt;Capture screenshots less frequently&lt;/h3&gt;
&lt;p&gt;I have yet to run into this limitation but I could see how, if your site is very large and/or if you commit very frequently, you may be concerned about exceeding Percy’s free quota (5000 screenshots/month).&lt;/p&gt;
&lt;p&gt;I have not had to handle this in any special way so far, but here a few options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Percy grabs screenshots of each page on your site in both Chrome and Firefox to ensure your site behaves well across browsers. You may decide you are comfortable with taking the risk of having smaller issues undetected and grab screenshots only on one of the two. This will mean you will consume half as many snapshots every time you run visual tests.&lt;/li&gt;
&lt;li&gt;Percy will also test your site on a couple different viewport sizes. This is helpful to ensure your site works well on desktop and mobile devices. Again, you may be comfortable with just running tests on one configuration in order to reduce resource consumption by half.&lt;/li&gt;
&lt;li&gt;You may configure your CircleCI workflow to &lt;a href=&quot;https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval&quot;&gt;toggle the snapshot step manually&lt;/a&gt; and run it only when you have meaningful changes to test (e.g. if you are adding new content or upgrading your theme). If you do this, you still want to make sure you refresh your screenshots based on &lt;em&gt;master&lt;/em&gt; fairly often, otherwise you might find yourself with visual diffs that cover so many changes together that are no longer informative. And if you run this very infrequently, you might as well just choose to run the &lt;code&gt;percy&lt;/code&gt; command locally.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Realistically, for most personal sites, you can likely go a long way with the free quota. If you are considering this for a large corporate site, I would rather consider paying for a higher tier and get more snapshots rather than trying too hard to capture fewer and have a less informative workflow.&lt;/p&gt;
&lt;h2 id=&quot;tests-are-even-more-valuable-if-you-are-a-theme-developer&quot;&gt;Tests are even more valuable if you are a theme developer&lt;/h2&gt;
&lt;p&gt;If you are developing a theme that others are going to use, testing this way is likely to be even more impactful: you can save yourself quite a bit of time by having a way to catch issues before you ship a new version instead of relying on your users to report problems they run into after they upgrade.&lt;/p&gt;
&lt;p&gt;You can apply most of the suggestions above by making sure that you have an example site (the &lt;a href=&quot;https://sourcethemes.com/academic/&quot;&gt;Academic&lt;/a&gt; theme I use is great for this) that exercises most of the features in your theme, &lt;em&gt;especially the ones that are not enabled by default&lt;/em&gt;. This would also likely reduce the time you spend manually inspecting your pages to make sure they still render as expected.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This has been a great opportunity to learn about great tools that are available out there (I will definitely consider Percy for the next app I will build in my own time) and how they can help greatly even with sites that are &lt;a href=&quot;https://www.abahgat.com/tags/StaticGen&quot;&gt;statically generated&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have accomplished most of the goals I had in mind when I started playing with this. There is one item left open for future investigation (mainly, a way to ensure the RSS for my site is valid and well-formed) but the CircleCI workflow I set up gave me a good foundation I can extend to cover more tests.&lt;/p&gt;</content:encoded></item><item><title>Zing LED Smart Night Light</title><link>https://www.abahgat.com/blog/zing-night-light</link><guid isPermaLink="true">https://www.abahgat.com/blog/zing-night-light</guid><description>I liked these WiFi enabled, motion-sensing night lights so far, I only wish they had 3 more features.</description><pubDate>Mon, 18 Feb 2019 21:24:08 GMT</pubDate><content:encoded>&lt;p&gt;Several months ago I was looking for a night light when I stumbled upon &lt;a href=&quot;https://www.indiegogo.com/projects/zing-smart-night-light#/&quot;&gt;Zing’s Indiegogo page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The main feature I was looking for was for the light to activate automatically when I was walking past it and to turn off a few seconds later. Zing seemed to be able to do this and more: after seeing browsing the site, what intrigued me were the many possibilities for customization, the integration with &lt;a href=&quot;https://www.ifttt.com&quot;&gt;IFTTT&lt;/a&gt; and the fact that each light has a temperature sensor — which I was hoping I would eventually be able to access via API.&lt;/p&gt;
&lt;p&gt;Some features, such as automatic path lighting and the locator feature, were not a part of the decision.
Others, event notification in particular, I knew I would not use (I am trying to minimize the notifications I get while I am home).&lt;/p&gt;
&lt;p&gt;I got a pack of 3 on Indiegogo, hoping to receive them relatively soon. The wait turned out to be longer than I expected (shipping ended up being a few months late due to some complications in the production process) but I finally received my lights in October.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/box.DTskC9CD.jpg&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;2637&quot; height=&quot;2867&quot; srcset=&quot;https://www.abahgat.com/_astro/box.DTskC9CD_Z1i1lHr.webp 400w, https://www.abahgat.com/_astro/box.DTskC9CD_1tYH6L.webp 768w, https://www.abahgat.com/_astro/box.DTskC9CD_16H58c.webp 1024w, https://www.abahgat.com/_astro/box.DTskC9CD_Z1PDJIk.webp 2040w, https://www.abahgat.com/_astro/box.DTskC9CD_114ak7.webp 2637w, https://www.abahgat.com/_astro/box.DTskC9CD_ZAIOA5.webp 5274w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 2637px; max-height: 2867px; aspect-ratio: 0.9197767701430066; width: 100%;&quot; alt=&quot;The box the lights come in.&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; The box the lights come in. &lt;/figcaption&gt; &lt;/figure&gt;
&lt;p&gt;So far, I have been quite happy with them. I liked the fact that each light is configurable and offers a number of settings to customize that will help you make sure it works with your environment and preferences.&lt;/p&gt;
&lt;p&gt;I installed three lights (two are in bedrooms and a third is in the master bathroom) and set all of them to a warm, yellow glow. I initially configured the bathroom light to a multi-colored rotating pattern (see the screenshot below) but shortly after I opted for a more relaxing solid color and static pattern.&lt;/p&gt;
&lt;div id=&quot;gallery-28y&quot; class=&quot;gallery-wrapper not-prose&quot; data-images=&quot;[]&quot;&gt; &lt;div class=&quot;grid gap-2 my-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3&quot;&gt;  &lt;/div&gt; &lt;!-- Lightbox Modal --&gt; &lt;div class=&quot;lightbox-modal fixed inset-0 z-[9999] w-full h-full p-0 m-0 bg-transparent backdrop:bg-black/90 pointer-events-none hidden opacity-0 transition-opacity duration-300&quot; role=&quot;dialog&quot; aria-modal=&quot;true&quot;&gt; &lt;!-- Backdrop --&gt; &lt;div class=&quot;fixed inset-0 bg-black/90 backdrop-blur-sm pointer-events-auto modal-close-area&quot;&gt;&lt;/div&gt; &lt;div class=&quot;relative w-full h-full flex items-center justify-center p-4 pointer-events-none&quot;&gt; &lt;!-- Close Button --&gt; &lt;button class=&quot;absolute top-4 right-4 z-50 p-2 text-white/70 hover:text-white bg-black/50 rounded-full hover:bg-white/20 transition-all pointer-events-auto modal-close-btn&quot; aria-label=&quot;Close&quot;&gt; &lt;svg width=&quot;1em&quot; height=&quot;1em&quot; class=&quot;w-8 h-8&quot; data-icon=&quot;tabler:x&quot;&gt;   &lt;symbol id=&quot;ai:tabler:x&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M18 6L6 18M6 6l12 12&quot;/&gt;&lt;/symbol&gt;&lt;use href=&quot;#ai:tabler:x&quot;&gt;&lt;/use&gt;  &lt;/svg&gt; &lt;/button&gt; &lt;!-- Previous Button --&gt; &lt;button class=&quot;absolute left-4 z-50 p-3 text-white/70 hover:text-white bg-black/50 rounded-full hover:bg-white/20 transition-all pointer-events-auto prev-btn&quot; aria-label=&quot;Previous&quot;&gt; &lt;svg width=&quot;1em&quot; height=&quot;1em&quot; class=&quot;w-8 h-8&quot; data-icon=&quot;tabler:chevron-left&quot;&gt;   &lt;symbol id=&quot;ai:tabler:chevron-left&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;m15 6l-6 6l6 6&quot;/&gt;&lt;/symbol&gt;&lt;use href=&quot;#ai:tabler:chevron-left&quot;&gt;&lt;/use&gt;  &lt;/svg&gt; &lt;/button&gt; &lt;!-- Next Button --&gt; &lt;button class=&quot;absolute right-4 z-50 p-3 text-white/70 hover:text-white bg-black/50 rounded-full hover:bg-white/20 transition-all pointer-events-auto next-btn&quot; aria-label=&quot;Next&quot;&gt; &lt;svg width=&quot;1em&quot; height=&quot;1em&quot; class=&quot;w-8 h-8&quot; data-icon=&quot;tabler:chevron-right&quot;&gt;   &lt;symbol id=&quot;ai:tabler:chevron-right&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;m9 6l6 6l-6 6&quot;/&gt;&lt;/symbol&gt;&lt;use href=&quot;#ai:tabler:chevron-right&quot;&gt;&lt;/use&gt;  &lt;/svg&gt; &lt;/button&gt; &lt;!-- Main Image --&gt; &lt;img src=&quot;&quot; alt=&quot;Lightbox&quot; class=&quot;max-w-[90vw] max-h-[80vh] object-contain shadow-2xl rounded-sm pointer-events-auto lightbox-image transition-transform duration-300&quot;&gt; &lt;p class=&quot;lightbox-caption absolute bottom-4 left-0 right-0 text-center text-white bg-black/50 p-2 mx-auto max-w-3xl rounded backdrop-blur-sm pointer-events-auto opacity-0 transition-opacity duration-300&quot;&gt;&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; 
&lt;p&gt;The Zing app allows configuration of many parameters for the lights, such as the light color, intensitiy, spread (influencing how wide of an area the light would illuminate) and speed (for moving patterns).&lt;/p&gt;
&lt;p&gt;Unfortunately, the Android app seems to be lagging behind with respect to the iOS one in terms of functionality — more on this later.&lt;/p&gt;
&lt;div id=&quot;gallery-28y&quot; class=&quot;gallery-wrapper not-prose&quot; data-images=&quot;[]&quot;&gt; &lt;div class=&quot;grid gap-2 my-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3&quot;&gt;  &lt;/div&gt; &lt;!-- Lightbox Modal --&gt; &lt;div class=&quot;lightbox-modal fixed inset-0 z-[9999] w-full h-full p-0 m-0 bg-transparent backdrop:bg-black/90 pointer-events-none hidden opacity-0 transition-opacity duration-300&quot; role=&quot;dialog&quot; aria-modal=&quot;true&quot;&gt; &lt;!-- Backdrop --&gt; &lt;div class=&quot;fixed inset-0 bg-black/90 backdrop-blur-sm pointer-events-auto modal-close-area&quot;&gt;&lt;/div&gt; &lt;div class=&quot;relative w-full h-full flex items-center justify-center p-4 pointer-events-none&quot;&gt; &lt;!-- Close Button --&gt; &lt;button class=&quot;absolute top-4 right-4 z-50 p-2 text-white/70 hover:text-white bg-black/50 rounded-full hover:bg-white/20 transition-all pointer-events-auto modal-close-btn&quot; aria-label=&quot;Close&quot;&gt; &lt;svg width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; class=&quot;w-8 h-8&quot; data-icon=&quot;tabler:x&quot;&gt;   &lt;use href=&quot;#ai:tabler:x&quot;&gt;&lt;/use&gt;  &lt;/svg&gt; &lt;/button&gt; &lt;!-- Previous Button --&gt; &lt;button class=&quot;absolute left-4 z-50 p-3 text-white/70 hover:text-white bg-black/50 rounded-full hover:bg-white/20 transition-all pointer-events-auto prev-btn&quot; aria-label=&quot;Previous&quot;&gt; &lt;svg width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; class=&quot;w-8 h-8&quot; data-icon=&quot;tabler:chevron-left&quot;&gt;   &lt;use href=&quot;#ai:tabler:chevron-left&quot;&gt;&lt;/use&gt;  &lt;/svg&gt; &lt;/button&gt; &lt;!-- Next Button --&gt; &lt;button class=&quot;absolute right-4 z-50 p-3 text-white/70 hover:text-white bg-black/50 rounded-full hover:bg-white/20 transition-all pointer-events-auto next-btn&quot; aria-label=&quot;Next&quot;&gt; &lt;svg width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; class=&quot;w-8 h-8&quot; data-icon=&quot;tabler:chevron-right&quot;&gt;   &lt;use href=&quot;#ai:tabler:chevron-right&quot;&gt;&lt;/use&gt;  &lt;/svg&gt; &lt;/button&gt; &lt;!-- Main Image --&gt; &lt;img src=&quot;&quot; alt=&quot;Lightbox&quot; class=&quot;max-w-[90vw] max-h-[80vh] object-contain shadow-2xl rounded-sm pointer-events-auto lightbox-image transition-transform duration-300&quot;&gt; &lt;p class=&quot;lightbox-caption absolute bottom-4 left-0 right-0 text-center text-white bg-black/50 p-2 mx-auto max-w-3xl rounded backdrop-blur-sm pointer-events-auto opacity-0 transition-opacity duration-300&quot;&gt;&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; 
&lt;p&gt;The predictive path lighting feature has been quite disappointing so far. Whenever one of the lights turns on (because you are walking past it), all the other two will turn on as well, almost as if the model powering the feature today wasn’t any sophisticated than “if motion is detected, turn on &lt;em&gt;all&lt;/em&gt; lights”. Not a big deal, but it meant that I turned off the feature on the light in the other bedroom, since I did not want any of getting up to trigger the light in our daughter’s room.&lt;/p&gt;
&lt;p&gt;I haven’t gotten to try neither the locator or the notification feature advertised on Indiegogo: I am not sure whether they are supported or not.&lt;/p&gt;
&lt;p&gt;Unfortunately, the version of the lights I received shipped with an older firmware version that is affected by a couple issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;some settings (e.g. blue light reduction) are not persisted if the light loses power;&lt;/li&gt;
&lt;li&gt;the activity indicator for the WiFi module often flashes blue.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I must say the latter issue is quite annoying in a night light. If you think about it, having a bright blue LED flash unexpectedly is quite noticeable in a dark room and almost defeats the purpose of having a night light.&lt;/p&gt;
&lt;p&gt;I am told that updating the device firmware might help with both of these issues but unfortunately the Android application is unable to perform the update so far. I have been in touch with the Zing Support team to understand what workaround are available (other than procuring an iPhone) and I am hoping to hear back soon.&lt;/p&gt;
&lt;p&gt;All considered, I have been quite happy with Zing, provided that I manage to fix the issue with the WiFi module.&lt;/p&gt;
&lt;p&gt;The features that I wish it had at this point are all related to software and am hoping they might happen soon:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Being able to prevent the lights from turning on at daytime or when the room is already bright enough;&lt;/li&gt;
&lt;li&gt;IFTTT/Google Assistant integration;&lt;/li&gt;
&lt;li&gt;Being able to access the temperature sensor via API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are curious to check Zing out, they now have &lt;a href=&quot;https://zing.fm/&quot;&gt;an official site&lt;/a&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;UPDATE (January 2021)&lt;/strong&gt; I am still quite happy with the basic functionality of these lights. However, I can’t recommend them if you are an Android user.&lt;/p&gt;
&lt;p&gt;The Android application to control the lights has not received any updates in years. It also lacks several features that are present on the iOS version, such as controlling the lights based on a schedule, upgrading the device firmware and more.&lt;/p&gt;</content:encoded></item><item><title>Migrating From Wordpress to Hugo</title><link>https://www.abahgat.com/blog/migrating-from-wordpress-to-hugo</link><guid isPermaLink="true">https://www.abahgat.com/blog/migrating-from-wordpress-to-hugo</guid><description>After many years of running my site on Wordpress, I just migrated this site to Hugo. The migration was quite simple, this post outlines the main steps and offers a few helpful resources.</description><pubDate>Thu, 15 Mar 2018 00:35:57 GMT</pubDate><content:encoded>&lt;p&gt;After many years of running my own site on Wordpress, I finally pulled the trigger and decided to migrate to a different stack.&lt;/p&gt;
&lt;p&gt;Wordpress had been working quite well for me until I started to run into some with the hosted version and did not want to deal with having to set up and maintain my own server just for this site.&lt;/p&gt;
&lt;p&gt;When I found myself, unexpectedly, with some time to spare — rocking my newborn daughter back to sleep in the middle of the night — I took it as an opportunity to learn what kind of options are available for running simple websites in 2018. I had read so much about &lt;a href=&quot;https://www.smashingmagazine.com/2015/11/modern-static-website-generators-next-big-thing/&quot;&gt;static site generators&lt;/a&gt; and they seemed such a great fit for what I was trying to do, so I decided to give it a shot.&lt;/p&gt;
&lt;p&gt;I am surprised to see how far things have made it since when I last looked. If are interested in the current state of things, you can find a pretty good list on &lt;a href=&quot;http://staticgen.com&quot;&gt;StaticGen.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I had no shortage of alternatives to consider but I fairly quickly settled on setting my new site up with &lt;a href=&quot;https://gohugo.io&quot;&gt;Hugo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thankfully, the migration itself was not too daunting, I was able to complete most of it during the course of a few nights while holding a sleeping baby 😉&lt;/p&gt;
&lt;p&gt;In case you are considering doing the same migration, here an outline of the steps involved and a few articles I would recommend.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Decide whether you want to keep the same apperance or you are okay with selecting a theme you like and just exporting your comment. In my case, I decided to switch to a &lt;a href=&quot;https://themes.gohugo.io/academic/&quot;&gt;new theme&lt;/a&gt;, so I focused on mapping how my existing content would be organized in the theme I was migrating to.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Migrate your content to Markdown that Hugo can process. I found this article useful: &lt;a href=&quot;https://sourcethemes.com/academic/docs/migrate-from-wordpress/&quot;&gt;Migrating from Wordpress&lt;/a&gt;. Requires installing a plugin on your Wordpress site to export content in a format that &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; (another static site generator) can process and then transform that to the format Hugo expects&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;alert alert-warning&quot;&gt;If your site is on wordpress.com, the guide above won&apos;t work as is, since you will not be able to install plugins unless you are hosting your own server. I worked my way around this by [exporting an XML dump of my site](https://en.support.wordpress.com/export/), and then starting up a throwaway wordpress server (I did this with cloud9 when they offered a free plan, you can probably get a similar result by running it on [docker](https://docs.docker.com/compose/wordpress/)).&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Your site will likely require some fixes at this point. The specifics depend on what it looks like but it is likely that you will want at least to verify that the links between pages are working fine. Images often require some fixes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;My site had a fair number of incoming links from other places. I wanted to avoid breaking them if possible. This is where I was glad I was deploying my site on &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt;, since they offer great support of &lt;a href=&quot;https://www.netlify.com/docs/redirects/&quot;&gt;Redirect &amp;amp; Rewrite Rules&lt;/a&gt;, among many other features.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I had a good number of comments on my old site and I wanted to carry them over. For the sake of simplicity, I chose to use Disqus for my comments and thankfully they had a good article about &lt;a href=&quot;https://help.disqus.com/en/articles/1717131-importing-comments-from-wordpress&quot;&gt;Importing comments from WordPress&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;alert alert-note&quot;&gt;Disqus comments are associated with page URLs, so you will want to make sure your pages are served at the same URLs as before the migration. Alternatively, you can edit the URLs in the export file before importing it following the instructions above.&lt;/div&gt;
&lt;p&gt;I have yet to find a technical migration that completes without introducing new issues, so if you ever encounter any bugs on this site, I would ask you to &lt;a href=&quot;https://www.abahgat.com/#contact&quot;&gt;please let me know&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Feel free to leave a comment if you are trying to do the same migration and you run into trouble, I can try to help you out.&lt;/p&gt;</content:encoded></item><item><title>What’s wrong with Milan’s Open Data initiative</title><link>https://www.abahgat.com/blog/whats-wrong-with-milans-open-data-initiative</link><guid isPermaLink="true">https://www.abahgat.com/blog/whats-wrong-with-milans-open-data-initiative</guid><description>I spent some time playing with the Open Data published by the City of Milan, aiming to visualize public transport coverage. While I managed to create a heatmap, I was left unsatisfied by the data presentation and format. The initiative is promising but could be dramatically improved by adopting modern standards like GeoJSON instead of Shapefiles and including simple preview capabilities.</description><pubDate>Thu, 12 Sep 2013 10:38:51 GMT</pubDate><content:encoded>&lt;p&gt;I spent some time during the last weeks playing with the &lt;a href=&quot;http://dati.comune.milano.it&quot;&gt;Open Data published by the City of Milan&lt;/a&gt;. I did not have a clear goal in mind, except for building some interesting visualization of the Public Transport coverage of the city grounds.&lt;/p&gt;
&lt;p&gt;A quick exploration of the dataset seemed to be encouraging: while most of the data was relatively useless, some datasets were indeed promising and worth spending some time. While at the end of the week I was able to get the result I had in mind (the heatmap below), I was left with that lingering feeling of dissatisfaction that accompanies me when I see good initiatives that can be dramatically improved by changing a few specific features.&lt;/p&gt;
&lt;figure class=&quot;my-6&quot;&gt; &lt;img src=&quot;https://www.abahgat.com/_astro/milan-bus-stops.7OyF_V9N.png&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; width=&quot;1026&quot; height=&quot;666&quot; srcset=&quot;https://www.abahgat.com/_astro/milan-bus-stops.7OyF_V9N_1U3lhR.webp 400w, https://www.abahgat.com/_astro/milan-bus-stops.7OyF_V9N_2lxFUE.webp 768w, https://www.abahgat.com/_astro/milan-bus-stops.7OyF_V9N_ZfzWIa.webp 1024w, https://www.abahgat.com/_astro/milan-bus-stops.7OyF_V9N_MSVsT.webp 1026w, https://www.abahgat.com/_astro/milan-bus-stops.7OyF_V9N_Z19w2fW.webp 2040w, https://www.abahgat.com/_astro/milan-bus-stops.7OyF_V9N_Z14Jz53.webp 2052w&quot; sizes=&quot;(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px&quot; style=&quot;object-fit: cover; object-position: center; max-width: 1026px; max-height: 666px; aspect-ratio: 1.5405405405405406; width: 100%;&quot; alt=&quot;Density of bus stops in Milan&quot; class=&quot;mx-auto rounded-md shadow-lg bg-border-default dark:bg-card w-full&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt; &lt;figcaption class=&quot;mt-2 text-center text-sm text-subtle dark:text-muted&quot;&gt; Density of bus stops in Milan &lt;/figcaption&gt; &lt;/figure&gt;
&lt;h2 id=&quot;presentation-of-data&quot;&gt;Presentation of data&lt;/h2&gt;
&lt;p&gt;If the purpose of a website is to publish data, data should be at the center. However, while CSV data sets featured a preview option, there was absolutely no way to preview topological data.&amp;nbsp;&lt;span style=&quot;font-style:inherit;line-height:1.625;&quot;&gt;Of course geographical displays are a more complex problem to solve but as of 2013 there are many libraries that can effortlessly visualize geographical features.&lt;/span&gt; Topological data is presented in a textual catalogue, with abundant descriptions and numerous fields of metadata, but &lt;strong&gt;there is no map&lt;/strong&gt;.&amp;nbsp;The screenshot below is the page on the website that describes the data about Parco Nord (a park where I used to go running). Note that it does not offer any hint about what the data look like. &lt;img src=&quot;https://www.abahgat.com/img/wp-uploads/2013/09/screen-shot-2013-09-12-at-11-18-33-am-e1378977609386.png&quot; alt=&quot;Parco Nord Page&quot; loading=&quot;lazy&quot;/&gt; Compare this with the element below: (almost) the same data visualized on GitHub as a GeoJSON file. I believe this format is much more effective in communicating what the data look like. I suspect you will agree with me.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Embedded content not available in RSS — &lt;a href=&quot;https://www.abahgat.com/blog/whats-wrong-with-milans-open-data-initiative&quot;&gt;view on site&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt; 
&lt;h2 id=&quot;choosing-the-right-format&quot;&gt;Choosing the right format&lt;/h2&gt;
&lt;p&gt;Topological data offered by the initiative is coded using the &lt;a href=&quot;http://en.wikipedia.org/wiki/Shapefile&quot;&gt;Shapefile&lt;/a&gt; data format, introduced in the 1990s for use with desktop GIS software. It is a very rich and powerful format but it encodes data as a set of compressed binary files, making it unusable with modern web applications without doing &lt;a href=&quot;http://ben.balter.com/2013/06/26/how-to-convert-shapefiles-to-geojson-for-use-on-github/&quot;&gt;some prior processing&lt;/a&gt;. While Shapefiles are great for professional GIS users, for an Open Data initiative to reach the most developers, using a text based format like &lt;a href=&quot;https://developers.google.com/kml/documentation/&quot;&gt;KML&lt;/a&gt; or&amp;nbsp;&lt;a href=&quot;http://www.geojson.org/&quot;&gt;GeoJSON&lt;/a&gt;&amp;nbsp;would have been a wiser choice, as it lowers the barrier for the general public to consume open data information. Both formats are sufficiently rich to encode structured information: the map below is a good example (and the &lt;a href=&quot;https://gist.github.com/abahgat/6359868/raw/3b693d0b179338054ab4385f513f2c2298a991bd/rete-metro-milano.geojson&quot;&gt;raw file&lt;/a&gt; is still human-readable).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Embedded content not available in RSS — &lt;a href=&quot;https://www.abahgat.com/blog/whats-wrong-with-milans-open-data-initiative&quot;&gt;view on site&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt; 
&lt;h2 id=&quot;the-end-result&quot;&gt;The end result&lt;/h2&gt;
&lt;p&gt;After spending some time on this I ended up creating a &lt;a href=&quot;http://abahgat.github.io/opendata-milano/&quot;&gt;GitHub repository&lt;/a&gt; with the data I played with converted to GeoJSON, ready for use with web applications, and wrote a simple visualization of the &lt;a href=&quot;http://abahgat.github.io/opendata-milano/experiments/transport.html&quot;&gt;coverage of the city of Milan by the public transport network&lt;/a&gt; (the image you can see at the beginning of this post). Now, it would be great if whoever is responsible for Milan’s Open Data could look into making information available through better formats, leveraging &lt;a href=&quot;https://mapsengine.google.com/&quot;&gt;Google Maps Engine&lt;/a&gt; or GitHub’s support for GeoJSON. While we wait for that to happen, if you convert more data to GeoJSON, feel free to fork &lt;a href=&quot;https://github.com/abahgat/opendata-milano&quot;&gt;opendata-milano&lt;/a&gt; on GitHub and contribute there.&lt;/p&gt;</content:encoded></item><item><title>Appsterdam Guru Session: Google App Engine for beginners</title><link>https://www.abahgat.com/blog/appsterdam-guru-session-google-app-engine-for-beginners</link><guid isPermaLink="true">https://www.abahgat.com/blog/appsterdam-guru-session-google-app-engine-for-beginners</guid><description>One of the things I was not expecting when I moved to Amsterdam was its active and vibrant tech community. Appsterdam, a non-profit organization focused around aggregating people with a passion for te...</description><pubDate>Sat, 06 Jul 2013 02:21:19 GMT</pubDate><content:encoded>&lt;p&gt;One of the things I was not expecting when I &lt;a href=&quot;https://www.abahgat.com/post/2012-11-20-what-you-should-know-before-moving-to-amsterdam/index.md&quot; title=&quot;What you should know before moving to&amp;nbsp;Amsterdam&quot;&gt;moved to Amsterdam&lt;/a&gt; was its active and vibrant tech community. &lt;a href=&quot;http://appsterdam.rs/&quot;&gt;Appsterdam&lt;/a&gt;, a non-profit organization focused around aggregating people with a passion for technology, is probably one of the central forces in this movement.&lt;/p&gt;
&lt;p&gt;In my year in Amsterdam I had been to a few meetups organized by people from Appsterdam and always came back home having learned something new. This is why when my colleague &lt;a href=&quot;https://twitter.com/mattfgl&quot;&gt;Matt&lt;/a&gt; (who himself is quite an active Appsterdam member) talked me into presenting a guru session on Google App Engine, I saw that as an opportunity to return the favor.&lt;/p&gt;
&lt;p&gt;While I tried to give an overview of App Engine in general (and the Python flavor, specifically), I also wanted to offer attendees the chance to work on some examples that were more interesting than the typical guestbook application that comes with all the tutorials you can find online.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/abahgat/gae4beginners-demos&quot;&gt;code examples&lt;/a&gt; build on two of the many APIs App Engine has to offer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;a href=&quot;https://developers.google.com/appengine/docs/python/channel/&quot;&gt;Channel API&lt;/a&gt; to build a web page that displays the current cursor position of every user looking at that site,&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/appengine/docs/python/endpoints/&quot;&gt;Google Cloud Endpoints&lt;/a&gt; to implement a simple REST-like backend for a webpage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can check out the &lt;a href=&quot;https://speakerdeck.com/abahgat/google-app-engine-for-beginners&quot;&gt;slide deck&lt;/a&gt; below and get the code examples from the &lt;a href=&quot;https://github.com/abahgat/gae4beginners-demos&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Embedded content not available in RSS — &lt;a href=&quot;https://www.abahgat.com/blog/appsterdam-guru-session-google-app-engine-for-beginners&quot;&gt;view on site&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;App Engine is a lot more than an advanced infrastructure to deploy applications: the numerous APIs and services it offers can enable developers to build advanced applications with limited effort. I hope this presentation, while just scratching the surface, gives you a glimpse on the possibilities.&lt;/p&gt;
&lt;hr&gt;
&lt;p style=&quot;font-size:smaller;&quot;&gt;
  Special thanks to &lt;a href=&quot;https://twitter.com/mattfgl&quot;&gt;Matt&lt;/a&gt; for pushing me to do this and &lt;a href=&quot;https://twitter.com/chiya_serena&quot;&gt;Serena&lt;/a&gt; for her help with example 2.
&lt;/p&gt;</content:encoded></item><item><title>Presenting Professional Invaders</title><link>https://www.abahgat.com/blog/presenting-professional-invaders</link><guid isPermaLink="true">https://www.abahgat.com/blog/presenting-professional-invaders</guid><description>The story of Professional Invaders, the game we built during TNW Conference&apos;s Hack Battle.</description><pubDate>Thu, 06 Jun 2013 07:25:00 GMT</pubDate><content:encoded>&lt;p&gt;A few weeks ago I attended&amp;nbsp;&lt;a href=&quot;http://thenextweb.com/conference/&quot; target=&quot;_blank&quot;&gt;The Next Web Conference&lt;/a&gt;&amp;nbsp;in Amsterdam and joined a bunch of fellow programmers for another edition of the Kings of Code Hack Battle, the same kind of event as the one where &lt;a title=&quot;Story of a hack: Bring Your Own&amp;nbsp;Music!&quot; href=&quot;https://www.abahgat.com/blog/story-of-a-hack-bring-your-own-music/&quot; target=&quot;_blank&quot;&gt;Bring Your Own Music&lt;/a&gt;&amp;nbsp;was born.&lt;/p&gt;
&lt;p&gt;Following the usual schedule, after a brief presentation from the API partners (Spotify, SendGrid, Braintree, Deezer, Pearson, Nokia, Rebtel, Bol.com, Smart TV Alliance and LinkedIn), all the attendees started evaluating ideas about what to build.&lt;/p&gt;
&lt;blockquote class=&quot;instagram-media&quot; data-instgrm-captioned=&quot;&quot; data-instgrm-permalink=&quot;https://www.instagram.com/p/Yf2NZsjz_5/?utm_source=ig_embed&amp;amp;utm_campaign=loading&quot; data-instgrm-version=&quot;13&quot; style=&quot; background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:540px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);&quot;&gt;&lt;div style=&quot;padding:16px;&quot;&gt; &lt;a href=&quot;https://www.instagram.com/p/Yf2NZsjz_5/?utm_source=ig_embed&amp;amp;utm_campaign=loading&quot; style=&quot; background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;&quot; target=&quot;_blank&quot;&gt; &lt;div style=&quot; display: flex; flex-direction: row; align-items: center;&quot;&gt; &lt;div style=&quot;background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot;display: flex; flex-direction: column; flex-grow: 1; justify-content: center;&quot;&gt; &lt;div style=&quot; background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot; background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;padding: 19% 0;&quot;&gt;&lt;/div&gt; &lt;div style=&quot;display:block; height:50px; margin:0 auto 12px; width:50px;&quot;&gt;&lt;svg width=&quot;50px&quot; height=&quot;50px&quot; viewBox=&quot;0 0 60 60&quot; version=&quot;1.1&quot; xmlns=&quot;https://www.w3.org/2000/svg&quot; xlink=&quot;https://www.w3.org/1999/xlink&quot;&gt;&lt;g stroke=&quot;none&quot; stroke-width=&quot;1&quot; fill=&quot;none&quot; fill-rule=&quot;evenodd&quot;&gt;&lt;g transform=&quot;translate(-511.000000, -20.000000)&quot; fill=&quot;#000000&quot;&gt;&lt;g&gt;&lt;path d=&quot;M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div style=&quot;padding-top: 8px;&quot;&gt; &lt;div style=&quot; color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;&quot;&gt; View this post on Instagram&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;padding: 12.5% 0;&quot;&gt;&lt;/div&gt; &lt;div style=&quot;display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;&quot;&gt;&lt;div&gt; &lt;div style=&quot;background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);&quot;&gt;&lt;/div&gt; &lt;div style=&quot;background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot;background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;margin-left: 8px;&quot;&gt; &lt;div style=&quot; background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot; width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;margin-left: auto;&quot;&gt; &lt;div style=&quot; width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);&quot;&gt;&lt;/div&gt; &lt;div style=&quot; background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);&quot;&gt;&lt;/div&gt; &lt;div style=&quot; width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div style=&quot;display: flex; flex-direction: column; flex-grow: 1; justify-content: center; margin-bottom: 24px;&quot;&gt; &lt;div style=&quot; background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 224px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot; background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 144px;&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/a&gt;&lt;p style=&quot; color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;&quot;&gt;&lt;a href=&quot;https://www.instagram.com/p/Yf2NZsjz_5/?utm_source=ig_embed&amp;amp;utm_campaign=loading&quot; style=&quot; color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;&quot; target=&quot;_blank&quot;&gt;A post shared by Alessandro Bahgat (@abahgat)&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/blockquote&gt; 
&lt;p&gt;I teamed up with &lt;a href=&quot;http://alexdeleon.name/&quot;&gt;Alexander&lt;/a&gt;, a friend of mine I already had the chance to work with back in the days when I when I was consulting.&lt;/p&gt;
&lt;p&gt;Having LinkedIn among the sponsors seemed to encourage us to build serious applications for serious professionals, but after discarding a few alternatives that would have been better projects for a Startup Weekend than a hackathon, we decided to take the opposite direction: building the silliest possible thing with the APIs we had access to.&lt;/p&gt;
&lt;p&gt;We eventually decided to work on a game and tried to build a Space Invaders clone that would let you throw paper balls at your professional connections.&lt;/p&gt;
&lt;p&gt;After some research, we found a well written Space Invaders implementation on GitHub (thanks &lt;a href=&quot;https://github.com/Calamari&quot;&gt;Calamari&lt;/a&gt;) and we started adding the silliness to it.&lt;/p&gt;
&lt;p&gt;The first day we focused on getting the game to work as we expected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;each invader would be one of your connections on LinkedIn,&lt;/li&gt;
&lt;li&gt;a Boss would spawn every now and then,&lt;/li&gt;
&lt;li&gt;the game would have some sort of soundtrack (thanks Deezer),&lt;/li&gt;
&lt;li&gt;while in “Boss mode”, the game would have a distinctive appearance (blinking red background and a different theme song).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The second day we turned our attention to features that were just fun to build:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a coin slot where players could buy more coins with their own credit card (API courtesy of Braintree),&lt;/li&gt;
&lt;li&gt;an easter egg we planned to use in the demo: attendees could spawn the Boss by sending email to an address we set up for the occasion (thanks Sendgrid).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The video below (3:11) shows the major changes the application went through. It was created by replaying significant entries in the commit log and recording what the game looked like at that time.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Embedded content not available in RSS — &lt;a href=&quot;https://www.abahgat.com/blog/presenting-professional-invaders&quot;&gt;view on site&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We approached the deadline with only one objective: making people laugh. Despite some technical issues (amusing at a tech conference), we managed to demo our hack and people seemed to have liked it: the guys from Sendgrid even decided to award us with a prize 🙂&lt;/p&gt;
&lt;p&gt;You play the game &lt;a title=&quot;Play Professional Invaders&quot; href=&quot;http://alexdeleon.github.io/professional_invaders/&quot; target=&quot;_blank&quot;&gt;&lt;strong&gt;here&lt;/strong&gt;&lt;/a&gt;. This version is a slightly different from what we presented at the hack battle, since we decided to keep only the features that made sense if we were to offer it online.&lt;/p&gt;
&lt;p&gt;We hope you’ll have as much fun playing it as we had putting it together!&lt;/p&gt;</content:encoded></item><item><title>What Van Gogh can teach us about persistence</title><link>https://www.abahgat.com/blog/what-van-gogh-can-teach-us-about-persistence</link><guid isPermaLink="true">https://www.abahgat.com/blog/what-van-gogh-can-teach-us-about-persistence</guid><description>I visited the Van Gogh museum in Amsterdam recently and, to my surprise, I left the exposition having learned something that matters beyond art.</description><pubDate>Mon, 04 Mar 2013 13:33:43 GMT</pubDate><content:encoded>&lt;p&gt;I visited the Van Gogh museum in Amsterdam recently and, to my surprise, I left the exposition having learned something that matters beyond art.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;According to his &lt;a href=&quot;http://en.wikipedia.org/wiki/Van_Gogh&quot;&gt;biography&lt;/a&gt;,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Van Gogh began to draw as a child, and he continued to draw throughout the years that led up to his decision to become an artist. He did not begin painting until his late twenties, completing many of his best-known works during the last two years of his life. In just over a decade, he produced more than 2,100 artworks, consisting of &lt;a href=&quot;http://en.wikipedia.org/wiki/List_of_works_by_Vincent_van_Gogh&quot; title=&quot;List of works by Vincent van Gogh&quot;&gt;860 oil paintings&lt;/a&gt; and more than &lt;a href=&quot;http://en.wikipedia.org/wiki/Drawings,_water-colours_and_prints_by_Vincent_van_Gogh&quot; title=&quot;Drawings, water-colours and prints by Vincent van Gogh&quot;&gt;1,300 watercolors, drawings, sketches and prints&lt;/a&gt;. […]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Before focusing on painting, he worked as an art dealer, teacher and missionary. It wasn’t until he was 32 that he painted his first major work.&lt;/p&gt;
&lt;p&gt;He did not have the fortune of being recognized as a talented artist in his young age like Michelangelo and others and yet still he did not let go of his desire of becoming a painter. The thing that strikes most of the museum is the quantity of studies and sketches Van Gogh made throughout his live in order to improve his skills.&amp;nbsp;&lt;span style=&quot;font-style:inherit;line-height:1.625;&quot;&gt;He wanted to paint so much that&amp;nbsp;&lt;/span&gt;&lt;strong style=&quot;font-style:inherit;line-height:1.625;&quot;&gt;he kept practicing&lt;/strong&gt;&lt;span style=&quot;font-style:inherit;line-height:1.625;&quot;&gt;&amp;nbsp;and put so much effort in improving that it eventually paid off: he is now remembered as the author of&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-style:inherit;line-height:1.625;&quot;&gt;dozens of the most renown paintings of the history of art.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;In an age where the reference point to define an accomplishment is&amp;nbsp;starting a company at 16 and become a billionaire at 22, we risk underestimating the value of persistence.&amp;nbsp;Sure, he did not reach fame and success while he was alive, and his life was not what you would define “happy”. But if he had quit because he was not an accomplished painter in his young age, art now would certainly be very different from what we know.&lt;/p&gt;
&lt;p&gt;The works of V&lt;span style=&quot;font-style:inherit;line-height:1.625;&quot;&gt;an Gogh are&amp;nbsp;a proof that there is no such thing as &lt;/span&gt;&lt;strong style=&quot;font-style:inherit;line-height:1.625;&quot;&gt;being too late to accomplish something remarkable&lt;/strong&gt;&lt;span style=&quot;font-style:inherit;line-height:1.625;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;</content:encoded></item><item><title>Prettier source code on WordPress.com</title><link>https://www.abahgat.com/blog/prettier-source-code-on-wordpress-com</link><guid isPermaLink="true">https://www.abahgat.com/blog/prettier-source-code-on-wordpress-com</guid><description>Posting source code on WordPress.com is quite simple: the platform already provides an extremely easy to use shortcode called sourcecode, based on a fairly flexible syntax highlighter plugin. By looki...</description><pubDate>Mon, 21 Jan 2013 10:34:50 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.abahgat.com/img/wp-uploads/2013/01/screen-shot-2013-01-16-at-10-44-59-pm.png&quot; alt=&quot;Formatted source code&quot; loading=&quot;lazy&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Posting source code on WordPress.com is quite simple: the platform already provides an extremely easy to use shortcode called&amp;nbsp;&lt;a href=&quot;http://en.support.wordpress.com/code/posting-source-code/&quot;&gt;&lt;code&gt;sourcecode&lt;/code&gt;&lt;/a&gt;, based on a fairly flexible syntax highlighter plugin. By looking at the examples in the &lt;a href=&quot;http://en.support.wordpress.com/code/posting-source-code/&quot;&gt;documentation page&lt;/a&gt;, however, it is evident that the default styling used to render sources is quite old-fashioned and does not fit most modern themes.&lt;/p&gt;
&lt;p&gt;While the shortcode offers options to allow users to control many options of the rendering, it does not allow us to configure colors, fonts and size (the default size is so tiny that it is barely readable on high-resolution screens).&lt;/p&gt;
&lt;p&gt;When I was writing the previous technical post, I did some investigations to figure out what options are available to post more readable sources if your blog is hosted on WordPress.com and I found out there are basically two alternatives.&lt;/p&gt;
&lt;h1 id=&quot;embedding-gists&quot;&gt;Embedding Gists&lt;/h1&gt;
&lt;p&gt;The easiest option is to rely on &lt;a href=&quot;https://gist.github.com/&quot;&gt;Gist&lt;/a&gt;&amp;nbsp;– GitHub’s tool for sharing snippets of code – which offers an&amp;nbsp;extremely easy way to embed code in your blog. Just create a new snippet (gist) there and &lt;a href=&quot;http://en.support.wordpress.com/gist/&quot;&gt;follow the instructions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Unfortunately, the gist embed shortcode available on WordPress.com is less flexible than what you would get if you installed it as a &lt;a href=&quot;http://wordpress.org/extend/plugins/embed-github-gist/&quot;&gt;plugin&lt;/a&gt; on your own instance of WordPress, but it will be enough for most cases.&lt;/p&gt;
&lt;table&gt;&lt;tr&gt;&lt;th&gt;&lt;p&gt;Pros&lt;/p&gt;&lt;/th&gt;&lt;th&gt;&lt;p&gt;Cons&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;p&gt;Easy to embed source&lt;/p&gt;&lt;/td&gt;&lt;td&gt;&lt;p&gt;Suitable for posts with a few (long) code snippets&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;p&gt;Code looks good and is readable&lt;/p&gt;&lt;/td&gt;&lt;td&gt;&lt;p&gt;Does not always work perfectly with search engines&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;p&gt;Easy for readers to access raw code&lt;/p&gt;&lt;/td&gt;&lt;td&gt;&lt;p&gt;Does not work with&amp;nbsp;RSS and posts over email&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;h1 id=&quot;styling-source-code-by-customizing-your-css&quot;&gt;Styling source code by customizing your CSS&lt;/h1&gt;
&lt;p&gt;While Gists work great most of the time, they are a pain to create and maintain if you are working on a post that should include multiple short snippets of code. In that case, the amount of bookkeeping you have to do is significant (you will have to create and link many small chunks of code) and you may want to be able to manage your code right within the post.&lt;/p&gt;
&lt;p&gt;In that case, it may be more practical to fix the CSS theme used by the syntax&amp;nbsp;highlight plugin&amp;nbsp;to make it look post-2010. If you set your own custom CSS on WordPress.com, it will be supposed to be included as the last one to allow you to redefine the styles specified by the theme you are using.&lt;/p&gt;
&lt;p&gt;Unfortunately, the CSS used by the syntax highlight module was clearly not written with extensibility in mind, but quite the opposite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;all the style declarations it includes make use of &lt;code&gt;!important&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;the plugin will dynamically include its own CSS as the last item in the &lt;code&gt;head&lt;/code&gt; node, meaning that it will have preference on the custom one you define.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes sense in the original context –&amp;nbsp;the original syntax highlighter offered several themes you could choose from by including different stylesheets, but that feature is not available on WordPress.com –&amp;nbsp;but will make your life more difficult. You will need to add &lt;code&gt;!important&lt;/code&gt; to &lt;strong&gt;all the CSS declarations you redefine&lt;/strong&gt; and you will need to use CSS selectors that are &lt;strong&gt;more specific&lt;/strong&gt; than the ones used by the plugin. You will be able to see the final result at the end of this post.&lt;/p&gt;
&lt;p&gt;WordPress’s syntax highlight is not perfect, and some things are still quite annoying&amp;nbsp;(e.g. line numbers get in the way if you try selecting and copying source code). Most issues could be addressed by upgrading the plugin to use version 3 of &lt;a href=&quot;http://alexgorbatchev.com/SyntaxHighlighter/&quot;&gt;SyntaxHighlighter&lt;/a&gt; instead of the outdated version that is in use now, but it is something you will not be able to control unless the folks at Automattic decide to update it.&lt;/p&gt;
&lt;table&gt;&lt;tr&gt;&lt;th&gt;&lt;p&gt;Pros&lt;/p&gt;&lt;/th&gt;&lt;th&gt;&lt;p&gt;Cons&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;p&gt;It is necessary to have access to Custom CSS (which is a paid feature)&lt;/p&gt;&lt;/td&gt;&lt;td&gt;&lt;p&gt;Hard to copy sources without including line numbers (unless you disable them)&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;p&gt;Access to advanced features (highlight lines, toggle line number display)&lt;/p&gt;&lt;/td&gt;&lt;td&gt;&lt;p&gt;Search engines index sources with the post content&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;p&gt;Source can be styled according to preference&lt;/p&gt;&lt;/td&gt;&lt;td&gt;&lt;p&gt;Getting your CSS applied correctly can be difficult (but you can start from &lt;a href=&quot;https://gist.github.com/4464280&quot;&gt;here&lt;/a&gt;)&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;h1 id=&quot;what-did-i-choose&quot;&gt;What did I choose?&lt;/h1&gt;
&lt;p&gt;Here is the &lt;a href=&quot;https://gist.github.com/4464280&quot;&gt;stylesheet&lt;/a&gt; (embedded as a Gist) I am currently using &amp;nbsp;on this blog, based on the pygments theme used to style code at&amp;nbsp;&lt;a href=&quot;http://docs.python.org/&quot; title=&quot;docs.python.org&quot;&gt;docs.python.org&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Embedded content not available in RSS — &lt;a href=&quot;https://www.abahgat.com/blog/prettier-source-code-on-wordpress-com&quot;&gt;view on site&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt; 
&lt;p&gt;You can can see what the final result looks like &lt;del&gt;in this post about&amp;nbsp;&lt;a href=&quot;https://www.abahgat.com/post/2013-01-07-user-authentication-with-webapp2-on-google-app-engine/index.md&quot; title=&quot;User authentication with webapp2 on Google App&amp;nbsp;Engine&quot;&gt;User authentication with webapp2 on Google App Engine&lt;/a&gt;&lt;/del&gt;&amp;nbsp;and in the image at the beginning of this post.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Update: I since migrated this blog to a new system, and am using a completely different way to render source code.&lt;/em&gt;&lt;/p&gt;</content:encoded></item></channel></rss>