All entries

A field that dims the axis instead of listing what it found

The frozen search contract was unfrozen, lost its filters, gained a Ukrainian stemmer and came back as a layer over the lane — for 2,426 bytes of JavaScript.

Search had been sitting behind a banner since the repository was four days old: a module nothing imported, kept and tested on purpose, under a project rule forbidding both its deletion and any claim that search worked. This pass took the banner off in two steps — the contract first, then the thing a reader can use.

The filters were cancelled, not built

The frozen module had two halves. One was a search over the text; the other was a set of filters — pick organizations, pick people, OR inside a group and AND across groups — with a walk over owned_by and developed_by links so that choosing a company also caught the models it made.

The filters went. Not deferred again: cancelled. A reader looking for OpenAI types “OpenAI”; they do not open a form, find the organization in a list of several hundred and tick it. Putting those names in the searchable text does the same work with no interface at all, and the graph walk existed only to feed the facets. A third branch searched tags, and the collection holds none: 483 records, zero tags. That branch had never matched anything.

What was left is one group of tokens, ANDed, over titles, summaries and the names a record mentions.

No index, because the scan is already faster than the frame

The obvious move for search on a static site is Pagefind, and before that MiniSearch, FlexSearch, Fuse or Orama. None of them earn their place here.

483 records answer a four-token query in 0.07 ms by plain scanning. A trie or a BM25 table would buy ranking this interface does not use — results are not a list to be ordered, they are marks that keep their position in time — and would spend bytes on a problem that does not exist. Pagefind is the closer call and fails for a different reason: it indexes rendered HTML and returns page URLs. The axis needs record ids to light marks with. A list of links beside the lane is a second navigation, not a search of the first.

What is precomputed is the haystack, because stemming is the slow half: 12.8 ms once when the deferred text lands, then 0.15–0.26 ms a keystroke.

Ukrainian is the reason a stemmer exists at all

The language inflects on the suffix. “модель”, “моделі” and “моделями” are one word to a reader and three to a substring match, and a reader typing any of them means all three. English inflects by adding to the end, which prefix matching already covers, so it gets no stemmer.

Snowball has no Ukrainian — which quietly rules out Pagefind and every other consumer of rust-stemmers, since they would fall back to no stemming at all for the atlas’s primary language. Orama ships one as a standalone module with no dependencies: 731 bytes gzipped. It is vendored byte-identical rather than installed, because the package carries twenty-eight languages to supply one, and excluded from Prettier so it stays identical to the release it came from.

It brings one thing worth knowing. “роботи” — work, in the genitive — and “робот”, a robot, share a stem. Searching for robots returns records about scientific work. That is what stemming is rather than a defect in the wiring, and the topic the axis already colours by is what will eventually separate them.

The layout had no room, so the field does not take any

The toolbar is measured to the pixel: below 420px it has already dropped the record count, below 380px the reset. The grid under it gives every remaining row to the lane, which is what the first-viewport acceptance measures. There was no row to put a search field in without taking it from one of those two.

So the panel floats over the lane, the button that opens it becomes a glyph under 420px while keeping its accessible name, and results are not listed at all. Matching marks stay lit, everything else falls to 22%, and the caption states the count. A dimmed mark is still a record at that date and still opens; what the search changes is emphasis, not the map. The reading list has no positions to keep, so there the same search drops rows instead of greying them.

One thing only a phone shows: on a narrow screen time runs upwards, so the newest decade — the one most searches answer with — sits exactly where the panel floats. The lane starts 108px lower while the field is open. As margin on the plot rather than padding on the frame, because the frame’s measured box drives the drawing and shrinking it would repack every mark on each open.

Three things the pass turned up

The island was still building time, event and mode by hand, which is precisely the drift the frozen banner had predicted in writing. Both now go through the contract, and callers name only the field they are changing.

A search field is the first text input this page has ever carried, and the axis binds bare printable keys as commands. Typing “GPT-4” zoomed the axis out. The handler now stands down inside an input — a bug that could not have existed before this pass and would have shipped without the manual pass over the keyboard.

The public-artifact gate refused the build outright until names was added to its allowlist. That is the gate working as designed: a field does not leave this build merely because some code put it in the projection.

What it cost

2,426 bytes of JavaScript, gzipped, for the entire feature including the stemmer — 88,747 of a 184,320 budget. The entity names added 7,036 bytes to the Ukrainian data.json, 51,389 of 122,880. LCP 1803 ms, CLS 0.026, 63 browser cases green.

The About page said “search and filters are deferred” for ten days. It was left alone in the first step, because the contract being ready is not the same as a reader being able to type — and a page that claims a feature it does not have is the one thing this atlas is not allowed to be. It changed in the second step, in the same commit that put the field on the screen.

Entry written September 21, 2026

Commits this entry accounts for

  • 9071111
  • 5825a78