- Scheme 34.2%
- JavaScript 25.7%
- Rust 14.6%
- Tree-sitter Query 8.4%
- Python 5.9%
- Other 11.2%
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.
- draft-saywhere-geocoding-02 — complete algorithm with pseudocode, test vectors, ABNF grammar, and security considerations (datatracker)
- RFC-SAYWHERE-001 — alternative RFC-format specification
- ADR-001 — algorithm design rationale
- SPEC-002 — feature requirements
- MODEL-004 — data model
- ENDPOINT-004 — REST API specification
- IMPL-005 — implementation plan
- Specs directory structure — organization and conventions
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}
}