what3words shuffles its vocabulary deliberately, so that table.chair.lamp and table.chair.lamps land on different continents. Ooru does the opposite: words are laid out so that neighbouring cells hold neighbouring meanings. This page describes how, and — more importantly — how much it actually worked.
One semantic domain per level
Each level draws from a disjoint domain. This is not decoration: it means a bare word announces which level it belongs to, so a transposed address can be rejected at parse time rather than resolved to somewhere plausible and wrong.
| Level | Domain | Words | Example neighbourhood |
|---|---|---|---|
| 1 · Region | landscape & water | 120 | dell, pass, glen, heath, knoll |
| 2 · Locality | materials, colour & texture | 400 | spruce, cedar, rosewood, cobble, balsa |
| 3 · Spot | living things | 400 | papaya, mango, guava, tulip, hibiscus |
| 4 · Pinpoint | light & weather | 16 | dew, monsoon, squall, shower, breeze |
The pipeline
The layout is generated offline by scripts/build-lexicon.ts and committed, so no model or network call is involved in serving the site.
- Curate. Hand-written candidate lists per domain, tagged with rough semantic groups, deliberately over-supplied so the filter has room to work.
- Filter. Enforce the vocabulary contract: plain lowercase ASCII, 2–12 characters, globally unique across levels, and — the important one — Levenshtein distance at least 2 from every other accepted word anywhere in the system.
- Embed. Each word is encoded with
Xenova/all-MiniLM-L6-v2into a 384-dimensional vector. - Organise.A Kohonen self-organising map is trained over the level’s exact lattice, then a Hungarian optimal assignment decides which word sits in which cell.
- Measure. Report whether the result is actually more coherent than chance.
Why words are embedded inside a frame
Single words are embedded as “a photo of a {word}” rather than bare. This is the standard trick for coaxing a sentence encoder into behaving like a word encoder: a bare token gives the model no syntactic context, and the resulting vectors tend to cluster by morphology — words that look alike — rather than by meaning. That is precisely backwards for this purpose.
Why a SOM and not PCA
The task is to place N words on an N-cell grid so grid neighbours are semantic neighbours. Projecting the embeddings to 2-D with PCA and snapping to the grid is the obvious approach and it is measurably worse: PCA is a linear projection that preserves the two directions of greatest global variance and shreds the local neighbourhood structure that actually matters here.
A SOM is trained to preserve topology instead. Each lattice node holds a prototype vector in the full embedding space, and every update drags the winning node and its lattice neighbours toward the sample:
The lattice therefore folds itself through the embedding cloud like a sheet, and adjacent nodes end up holding similar prototypes by construction.
Why Hungarian assignment afterwards
The SOM alone does not give an assignment — several words can share a best-matching node and some nodes get none, which a grid cannot represent. A Hungarian (Kuhn–Munkres) solve over the word × node cost matrix finds the globally cost-minimal one-to-one mapping in O(n³); at n = 400 that is about 64 million operations, a few seconds offline. The SOM decides the shape of the map; the assignment decides who sits where.
Did it work?
Two measurements, both computed at build time over every pair of cells in each level and recorded in the shipped lexicon manifest.
| Level | Neighbour similarity | Random pair | Coherence lift | Rank correlation |
|---|---|---|---|---|
| 1 · Region | 0.5553 | 0.4528 | 1.226× | 0.3808 |
| 2 · Locality | 0.5354 | 0.4147 | 1.291× | 0.2126 |
| 3 · Spot | 0.5405 | 0.4088 | 1.322× | 0.2789 |
| 4 · Pinpoint | 0.5812 | 0.5192 | 1.119× | 0.3486 |
One thing that was tried and did not work
The obvious next move is to denoise before organising: a 400-word vocabulary drawn from a single domain cannot plausibly span 384 meaningful directions, so projecting onto the leading principal components first ought to let the SOM spend its capacity on real structure instead of on noise. It was implemented and measured. It made things worse.
| Level | Coherence lift — full 384d | with PCA→32d | Rank ρ — full | with PCA→32d |
|---|---|---|---|---|
| 1 · Region | 1.226× | 1.232× | 0.3808 | 0.3396 |
| 2 · Locality | 1.291× | 1.284× | 0.2126 | 0.1543 |
| 3 · Spot | 1.322× | 1.318× | 0.2789 | 0.2811 |
| 4 · Pinpoint | 1.119× | 1.012× | 0.3486 | 0.0443 |
The reading is that the discarded components were not noise. Distinctions this vocabulary genuinely depends on — the difference between a fabric and a mineral, between a bird and a fish — apparently live in directions of comparatively low global variance, exactly the ones PCA throws away first. The full-dimensional layout is what ships. The switch survives as bun run lexicon --reduce=32 so the result can be re-checked rather than taken on trust.
What it looks like
The clearest evidence is not a statistic but the level-1 grid itself. Read west to east and the vocabulary walks from water to relief without anyone having arranged it by hand:
The atlas shows every level in full, coloured by semantic cluster.
Meaning drives appearance too
The three principal components of each word’s embedding are stored alongside it, and the 3-D marker on the map derives its hue, its mass and its proportions from them. Two words that mean similar things therefore produce markers you would describe the same way. It is the same data doing two jobs: deciding where the word sits and what it looks like.
signature[0]→ hue, so the dominant semantic axis becomes the dominant colour axissignature[1]→ mass: huts in the village, tower height, storeys in the housesignature[2]→ taper, waist and twist