6
2
Fork
You've already forked saywhere
0
An open-source, deterministic algorithm that converts geographic coordinates to memorable word phrases and back.
  • Scheme 34.2%
  • JavaScript 25.7%
  • Rust 14.6%
  • Tree-sitter Query 8.4%
  • Python 5.9%
  • Other 11.2%
2026年07月11日 23:40:46 +10:00
data refactor: revise implementation 2025年10月10日 22:50:42 +11:00
rust rust: import saywhere-rs as rust/ (subtree, history preserved) 2026年07月11日 23:39:44 +10:00
saywhere verify the wordlist's canonical digest before use (spec MUST) 2026年07月11日 13:21:46 +10:00
scripts checksum: signal the method, weight by position, reduce mod 31 2026年07月10日 13:39:47 +10:00
server server: report the expected checksum for the method the word selects 2026年07月10日 14:03:22 +10:00
specs specs: fix hierarchy examples to coarse-first word order 2026年07月11日 21:44:41 +10:00
tests verify the wordlist's canonical digest before use (spec MUST) 2026年07月11日 13:21:46 +10:00
web Merge branch 'geo-qr-pin' 2026年07月11日 21:55:05 +10:00
.dockerignore fix: docker 2025年10月11日 11:02:59 +11:00
.gitignore checksum: signal the method, weight by position, reduce mod 31 2026年07月10日 13:39:47 +10:00
build-web.sh web: bound the map to one world; content-hash app.js 2026年06月22日 21:34:35 +10:00
expand_wordlist.py feat: guile implementation of saywhere RFC 2025年10月10日 18:25:50 +11:00
fly.toml fix: docker 2025年10月11日 11:02:59 +11:00
INSTALL.md feat: guile implementation of saywhere RFC 2025年10月10日 18:25:50 +11:00
LICENCE docs: update README to draft-02 vocabulary; add LICENCE file 2026年07月11日 23:37:29 +10:00
Makefile feat: web app WIP 2025年10月12日 09:36:12 +11:00
README.md readme: document the Rust implementation under rust/ 2026年07月11日 23:40:46 +10:00
README_IMPLEMENTATION.md feat: guile implementation of saywhere RFC 2025年10月10日 18:25:50 +11:00
saywhere.scm feat: guile implementation of saywhere RFC 2025年10月10日 18:25:50 +11:00
SAYWHERE_URN_EXAMPLES.md docs: update readme 2025年10月10日 23:58:04 +11:00
test-simple.scm feat: guile implementation of saywhere RFC 2025年10月10日 18:25:50 +11:00
USAGE.md feat: guile implementation of saywhere RFC 2025年10月10日 18:25:50 +11:00

SayWhere

Status: Experimental License: GPL-3.0 Guile: 3.0+ IETF I-D Web: saywhere.org

Reference implementation of SayWhere: an open, deterministic geocoding system that encodes geographic coordinates into memorable word phrases and decodes them back. SayWhere composes two established standards — geohash spatial indexing and the BIP-39 mnemonic wordlist — into hierarchical phrases with two-layer error detection. The same coordinates always produce the same words, phrases decode offline with nothing but the wordlist, and the optional terminal checksum can be verified by hand, so a location spoken over radio can be validated without electronic devices. Designed for emergency response, where digital systems may be unavailable and verbal location sharing is critical.

Quick start

Prerequisites: GNU Guile 3.0+ (see INSTALL.md).

git clone https://codeberg.org/anuna/saywhere.git
cd saywhere
make check # verify Guile installation
make test # run the test suites
make server # start the API server on localhost:8000

Encode coordinates:

curl "http://localhost:8000/api/v1/saywhere/encode?lat=40.7128&lon=-74.0060&words=3&checksum=true"
# Response (abridged):
# {
# "words": "grape.column.hip.hermit",
# "locationWords": "grape.column.hip",
# "wordCount": 3,
# "checksumWord": "hermit",
# "checksumAlgorithm": "crc8",
# "geo": {"latitude": 40.7128, "longitude": -74.0060},
# "precisionMeters": "~900m",
# "hierarchical": [...]
# }

Parameters: lat, lon (required), words (1-6, default 3), lang (default en), alt (metres), checksum (true to append a CRC-8 terminal checksum word).

Decode a phrase:

curl "http://localhost:8000/api/v1/saywhere/decode?words=grape.column.hip.hermit"
# Response (abridged):
# {
# "words": "grape.column.hip.hermit",
# "geo": {"latitude": 40.7128, "longitude": -74.0060},
# "checksumValid": true,
# "checksumAlgorithm": "crc8",
# "precisionMeters": "~900m"
# }

A batch endpoint is available at POST /api/v1/saywhere/batch. The hosted web app at saywhere.org runs the same algorithm entirely client-side. See USAGE.md for the full command-line reference.

The problem

Geographic coordinates are precise but hostile to human communication. A pair such as (40.7128, -74.0060) demands exact decimal transcription and offers no redundancy: a single misheard digit silently names a different place. Word-based geocodes fix memorability, but the dominant system is proprietary — the mapping from words to coordinates requires a licensed resolver, cells are deliberately non-hierarchical (adjacent squares share no structure), and there is no checksum, so transmission errors produce plausible wrong locations rather than detectable ones. Open alternatives (raw geohash, Plus Codes) are hierarchical and resolver-free but encode into alphanumeric strings nobody can remember or say over a noisy radio channel.

System Open algorithm Hierarchical Memorable Error detection
What3Words No No Yes None
Geohash Yes Yes No None
Plus Codes Yes Yes No None
SayWhere Yes Yes Yes Two-layer

The solution

SayWhere maps each pair of geohash characters (10 bits) plus one parity bit to a word in the 2,048-word BIP-39 wordlist. Because geohash is a prefix code over space, the phrases inherit a true containment property: each shorter phrase represents an area that contains every longer phrase sharing its prefix.

grape # ~900km region (contains all below)
grape.column # ~28km city (contains all below)
grape.column.hip # ~900m neighborhood (contains all below)
grape.column.hip.thought # ~27m building
grape.column.hip.thought.pull # ~90cm precise position

These are the real phrases for New York City (40.7128°N, 74.0060°W), the same location used by the specification's test vectors. The prefix property enables progressive disclosure — share only the precision the other party needs — and prefix-based search (WHERE phrase LIKE 'grape.column.%').

Error detection is layered on without extra words for the common case: every word carries one parity bit in its least significant index bit, and an optional terminal checksum word — drawn from a vocabulary disjoint from the location wordlist, so it is unambiguous — validates the whole phrase. One of the two checksum methods is computable by hand.

Features

  • Deterministic and reversible. Same coordinates always produce the same words; decoding recovers the cell exactly (roundtrip within grid precision).
  • Hierarchical precision. 1-6 words, from ~900km regions to ~2.7cm cells; shorter phrases spatially contain longer ones.
  • Two-layer error detection. Per-word LSB parity plus an optional terminal checksum (CRC-8 canonical, or a hand-computable additive method); the checksum word itself identifies which method produced it.
  • Hand-verifiable. The additive checksum needs only letter values and arithmetic below 62 — no wordlist, no device.
  • 3D positioning. Optional altitude suffix in metres above the WGS-84 ellipsoid, per RFC 5870.
  • Self-contained. No external API dependencies; the web app encodes and decodes entirely client-side. Wordlist integrity is enforced by SHA-256 digest verification before use.
  • Specified. IETF Internet-Draft with test vectors, ABNF grammar for the URN form, and security considerations.

Algorithm

Encoding. Validate latitude [-90, 90] and longitude [-180, 180]; compute the geohash; split it into 2-character chunks (10 bits each); append 1 parity bit (popcount mod 2) to each chunk; map each 11-bit value to a BIP-39 word; join with dots; optionally append a terminal checksum word.

Decoding. Split on dots; detect and strip a trailing checksum word (the checksum vocabulary is disjoint from BIP-39, so the test is unambiguous); look up each location word to recover its 11-bit index; verify per-word parity; validate the terminal checksum if present — the vocabulary the checksum word belongs to identifies the method (CRC-8 or additive), and exactly one is computed; reassemble the geohash; decode to coordinates with error bounds.

Precision

Words Geohash chars Grid dimensions Avg size Use case Example
1 2 1,252km ×ばつ 624km ~900km Country/large region grape
2 4 39.1km ×ばつ 19.5km ~28km City grape.column
3 6 1.2km ×ばつ 609m ~900m Neighborhood (recommended) grape.column.hip
4 8 38.2m ×ばつ 19m ~27m Building grape.column.hip.thought
5 10 1.2m ×ばつ 59.5cm ~90cm Room/precise position grape.column.hip.thought.pull
6 12 3.7cm ×ばつ 1.9cm ~2.7cm Sub-metre precision grape.column.hip.thought.pull.wave

Three words (~900m) balance memorability and precision for most uses. The conformance range is 1-6 location words: encoders MUST NOT emit more, and decoders MUST reject longer phrases. The encoding pattern generalizes beyond 6 words (each additional word adds ~2 geohash characters of precision while preserving the prefix relationship), but rows beyond 6 are non-normative and reserved for a future revision of the specification.

Geohash alternates longitude and latitude refinement, 5 bits per base-32 character. Maximum error at the equator by phrase length: 1 word ±630km, 2 words ±20km, 3 words ±610m, 4 words ±19m, 5 words ±60cm. Longitude error shrinks by cos(latitude) away from the equator.

Error detection

Layer 1: per-word parity

Every word encodes 10 bits of geohash data plus 1 even-parity bit (popcount mod 2) in its least significant bit. Single-bit errors in any word are caught during decode with no additional words transmitted.

Layer 2: terminal checksum

An optional terminal checksum word validates the whole phrase. It is drawn from one of two disjoint vocabularies, and which vocabulary it belongs to identifies the algorithm that produced it — so a validator computes exactly one checksum. Computing both and accepting either would roughly double the false-negative rate.

CHECKSUM_CRC_32 — the canonical CRC-8 method (32 words):

amber, ruby, scarlet, emerald, burgundy, crimson, lilac, indigo,
penguin, starling, walrus, chipmunk, platypus, standard, statue, rascal,
mustard, hamlet, reactor, shaman, spectrum, sonar, relic, tactic,
pilgrim, triad, orchid, tuxedo, outpost, pendant, hermit, surplus

CHECKSUM_ADD_31 — the hand-computable additive method (31 words; the additive sum reduces modulo the prime 31):

chestnut, hemlock, sycamore, poplar, jasper, bedrock, garnet, mantis,
shipyard, pretzel, fathom, stampede, tantrum, plankton, sawdust, tapestry,
stardust, tempest, gambit, octave, tarmac, tinder, poncho, tremor,
helix, stigma, remnant, nutmeg, anvil, cadence, trinket

Every one of these 63 words is exact- and prefix-disjoint from all ten official BIP-39 wordlists, at least two phonemes distant from every English location word (so gray/grape and seal/sea cannot arise), spellable from hearing it said aloud, and common in spoken English. scripts/checksum_vocab.py re-checks each rule mechanically.

Location: "grape.column.hip"
CRC-8: "hermit" (CRC-8 → index 30)
Additive: "tremor" (position-weighted sum → index 23)
Full phrase: "grape.column.hip.hermit" (device-generated)
 or "grape.column.hip.tremor" (hand-computed)

Both methods detect word substitution, omission, transposition, and addition at ~97%, uniform across error classes and word positions (CRC-8 residual 1/32, additive 1/31). Checksums detect accidental errors only — they provide no authentication, and an attacker can trivially generate valid phrases.

Hand calculation

The additive checksum multiplies each location word's letter sum by its 1-based position, sums the results, and reduces modulo 31. Position weighting makes it order-sensitive — a plain letter sum would validate every transposition. The modulus is prime so that every position weight is invertible and detection stays uniform at every position (modulo 32, gcd(4, 32) = 4 and a substitution in the fourth word would slip through ~12.7% of the time).

Example: "grape.column.hip"
Step 1: Letter values a=1, b=2, ..., z=26
Step 2: Sum every letter of each word, then multiply by its position
 grape = 7+18+1+16+5 = 47 x1 = 47
 column = 3+たす15+たす12+たす21+たす13+たす14 = 78 x2 = 156
 hip = 8+9+16 = 33 x3 = 99
Step 3: Total = 47 +たす 156 +たす 99 = 302
Step 4: Index = 302 % 31 = 23
Result: CHECKSUM_ADD_31[23] = "tremor"

Subtract 31 whenever the running total reaches 31 or more, and no step ever exceeds 61 — you never need to divide.

Altitude

Altitude is a signed value in metres above the WGS-84 ellipsoid, following the geo: URI scheme (RFC 5870) and GeoJSON (RFC 7946). One universally understood unit keeps a SayWhere URN self-describing — no hidden step sizes or unit configuration. Sub-metre precision is allowed with up to two decimals; the range is -30000 to +30000 metres. Altitude is not encoded into the words: it rides as an optional :metres suffix.

Ground entrance: grape.column.hip.thought
Drone at 20 m: grape.column.hip.thought:20
Parking level B2: grape.column.hip.thought:-6
Sub-metre: grape.column.hip.thought:20.5

Applications that think in floors or flight levels convert at their own boundary; the value on the wire is always metres.

URN format

SayWhere defines an RFC 8141-compliant URN form:

urn:saywhere:{language}:{word1}.{word2}...{wordN}[.{checksum}][:{altitude}]
# New York City - hierarchical (no checksum)
urn:saywhere:en:grape
urn:saywhere:en:grape.column
urn:saywhere:en:grape.column.hip
# With terminal checksum
urn:saywhere:en:grape.column.hip.hermit
# With altitude: 60 metres above the WGS-84 ellipsoid
urn:saywhere:en:grape.column.hip.hermit:60
# London (from the specification's test vectors)
urn:saywhere:en:kit.puzzle.marine.grit
# Equator/prime meridian (0°, 0°)
urn:saywhere:en:divert.zone.zone

In version 1.0.0 the only conformant language tag is en, permanently bound to the canonical English wordlist digest; future revisions may define additional tags, and the checksum vocabulary stays English for every wordlist. See the IETF draft for the ABNF grammar, parsing rules, and lexical-equivalence rules, and SAYWHERE_URN_EXAMPLES.md for a catalog of real-world examples.

Emergency use

Radio-only rescue. A hiker reads out "grape column hip tremor"; the dispatcher validates the hand-computable checksum word "tremor" with a printed reference card, confirms the location (40.7128°N, 74.0060°W), and dispatches — no computer required. Words are radio-clear, the checksum catches transmission errors, and the location can be pre-computed from maps.

Multi-level structures. Teams in a high-rise share one horizontal phrase and differ only in the altitude suffix: "grape column hip thought colon one-two-six" is ~126 m (~42nd floor), ":114" is ~38th floor, and the bare phrase is the ground entrance. The suffix is always metres; applications convert to floors at their own boundary.

Progressive disclosure. Share grape.column (~28km) to name the city, grape.column.hip (~900m) for the neighborhood, and grape.column.hip.thought (~27m) only when the other party needs the building.

Architecture

Pure, stateless GNU Guile Scheme. The core modules (saywhere/{core,geohash,wordlist,checksum,sha256}.scm) are deterministic and side-effect free; the HTTP server (server/, built on ice-9 web) and the web app are thin shells over the same algorithm. The web app (web/js/app.js) is a buildless, dependency-free single file that encodes, decodes, and validates both checksum methods entirely client-side. Wordlists are static JSON artifacts whose SHA-256 digest is verified against the canonical digest before use, in every implementation, as the specification requires.

A Rust implementation lives in rust/ — same algorithm, same draft-02 conformance (both checksum methods, digest verification, strict 1-6 decode), verified to produce output identical to the Guile reference by its equivalence and checksum_equiv binaries. Run cargo test from rust/ (153 tests).

Testing

make test # all suites
make test-geohash # 15 tests - geohash encoding/decoding
make test-wordlist # 13 tests - BIP-39 wordlist operations
make test-core # 36 tests - hierarchical encoding core
make test-urn-examples # 59 tests - real-world URN examples

123 tests across four suites. The URN example suite covers major cities, landmarks, edge cases (equator, poles, date line), and extreme locations. Known issue: on some Guile 3.0.x releases, SRFI-64 misreports test-error assertions as failures even though the implementation raises the expected error (verifiable manually); all positive-path tests pass.

Status and roadmap

Complete: hierarchical encoding/decoding (1-6 words), per-word parity, both terminal checksum methods with auto-detection, altitude support, wordlist digest verification, REST API with batch endpoint, client-side web app, Docker packaging, production deployment at saywhere.org, and the IETF draft with test vectors.

In progress: printable hand-calculation reference card, OpenAPI documentation, rate limiting, benchmarking and monitoring.

Future: additional language wordlists (requires a draft revision; language tags are permanently bound to wordlist digests), mobile SDKs, voice-optimized wordlists for radio.

Privacy and security

SayWhere phrases encode precise physical locations that persist over time. Implementations should not log or store phrases without explicit consent; phrases shared over radio or public channels should be considered public, and location transmission may be subject to pervasive monitoring (RFC 7258). Use hierarchical phrases to disclose only the precision required. The system itself is stateless and collects nothing: coordinates are processed on-the-fly, and the web app never sends coordinates to a server.

Checksums provide error detection, not security. The 5-bit terminal checksum (~3% residual) detects accidental corruption only; it is trivially forgeable and must not be relied on for authenticated location data — add cryptographic signatures at the application layer if you need that.

Wordlist integrity is safety-critical: a corrupted or substituted wordlist systematically mistranslates locations. Implementations MUST verify the wordlist's SHA-256 digest against the canonical digest for its version before use. See the draft's Security Considerations for the complete analysis.

Specifications

Development follows a specification-driven workflow; the IETF draft is the primary specification for implementers.

Contributing

Contributions are welcome, including reports of things that don't work or aren't clear. Issues and discussions live on Codeberg. Before opening a PR, run make test. If you're unsure how a change fits, open an issue; we're happy to help orient you.

License

GPL-3.0. See LICENCE.

Acknowledgements

SayWhere builds on established open technologies: Geohash (Gustavo Niemeyer, 2008) for spatial indexing, BIP-39 (Palatinus et al., 2013) for the 2,048-word mnemonic wordlist, and GeoWords by mfrank (Codeberg) as inspiration for the altitude component.

Citation

@misc{draft-saywhere-geocoding-02,
 title = {SayWhere Geocoding System: Human-Memorable Geographic Coordinates},
 author = {O'Connor, Hugo},
 year = {2026},
 howpublished = {Internet-Draft draft-saywhere-geocoding-02, IETF},
 url = {https://datatracker.ietf.org/doc/draft-saywhere-geocoding/},
 note = {Work in Progress}
}