BRC-515 Companion — MAP Location Keys

Companion to apps/0515.md · mechanism diagrams, verification method, open items · revised 2026-08-27 · published 2026-08-27 13:29 UTC
companion document §4 math independently reproduced 14 applied to the spec 3 remaining, all external

What this is. A companion to BRC-515 holding two things: diagrams of the mechanism the spec describes in prose, and the method and results of checking its arithmetic independently. The spec says what the protocol is; this shows how it behaves.

Section numbers in the form §4.3 refer to BRC-515. Other BRCs are cited by number.

Fourteen review findings have been applied to the spec and are recorded below for provenance. Three items remain, and every one of them lives outside this document.

Status

Remaining — four items, one resolved, none of them in BRC-515

#itemblocksrecommendation
R1MAP published two conflicting specsresolvedMerged. opldotdev/MAP master now carries the six-command set. BRC-515's citation is no longer ahead of the spec it cites.
R2BRC-503 is unmergedblockingLand opldotdev/BRCs PR #1. Until it does, BRC-515's primary citation has no number.
R3MAP state along a 1Sat chain was unwrittenin reviewDrafted as BRC-514 (PR #4, companion). §5 cites it; the interim rule is gone.
R4go-templates MAP parsermergedCommand-set fixes merged in both repos. ::: remains, tracked in 1sat-sdk#28.

Applied to BRC-515

wasnow in the spec
§5 re-implemented BRC-159's origin, transfer, tip and pairing, with an incompatible pairing rule§5 splits ownership (the asset's own model — 1Sat spend authority, an AIP or Sigma signature, or whatever a system defines) from position (this document's). Pairing table, Order and Tip removed.
“Only the holder can move it” was prose, with nothing stopping a stranger's SELECT from unplacing an objectAn indexer MUST ignore a position command it cannot authorize, and MUST state which model it enforces. See Where ownership comes from.
“The quadkey in effect at the tip” was defined only in a non-normative noteMoved out entirely. §5 now cites BRC-514, which owns the revision order, the state model and every command's fold.
Placement assumed a 1Sat token throughoutA placed object is any output whose resolved MAP state carries a quadkey. Spend authority is one binding, not the definition.
§1 and §5 disagreed about what unplaces an objectOne rule: removing quadkey unplaces. Transfers relocate nothing on their own.
DELETE used for a scalar key; no way to express multi-cell placement§2.1 maps the four MAP commands onto place / add a cell / drop a cell / unplace. quadkey is list-valued.
bare lat / lng forked MAP's namespace; no altitudecoordinates.lat / .lng / .alt, single-valued. Bare lat/lng forbidden.
myapp:<seed> recommended; a bare seed silently merged worldsGenesis outpoint preferred, hash second, myapp: a fallback. Bare seeds forbidden. app has no role in world identity.
silent on whether a move may change worldPortals specified: world may change, identity survives, one world at a time, arrival is not authorization.
multiple MAP commands per output undefinedCites BRC-503 §3's ::: separator.
empty world both forbidden and required to be acceptedOne clause with the write/read asymmetry stated.
numbered 229, below every dependencyBRC-515 — above the 500–513 corpus and clear of 314.
“placed output” had no 1-satoshi requirement, implying a second lineage modelScope settled: an authorization model must be in force, and the indexer must say which one it enforces. 1Sat is one option.
geohash aspect ambiguous; h3 unqualified; join tags duplicated MAP's listWording fixed; h3 is a cell index; join tags cite BRC-503 §5 instead of restating.

Where BRC-515 sits

BRC-515 is a key profile. It names attributes that ride on an existing attribute protocol, attached to an object whose identity and movement come from a different protocol again. Drawing that layering out shows where each part of the current draft belongs.

BRC-515 — MAP Location Keys quadkey · world · coordinates.lat + lng · geohash / h3 cell math · prefix and viewport query predicates BRC-503 — MAP key/value attributes on an output SET · ADD · SELECT · REMOVE · DELETE · CLEAR · ::: no ownership · no sequencing BRC-159 — 1Sat Ordinals origin · transfer by sat ordering · tip burn · new-origin detection ownership = spend authority BRC-500 — Bitcom protocol prefix · | pipeline carries the keys carries identity & movement BRC-159 is one available model
MAP supplies attributes and has no notion of who may change them. 1Sat supplies an owner and an ordered chain, and says nothing about what the attributes mean. BRC-515's contribution is the middle: which keys, what they encode, and what a region query returns. Ownership sits in the right-hand box — and 1Sat is one way to fill it, not the only one.

BRC-515's own contribution

The quadkey grammar and cell math (§4), the key semantics and the placing / non-placing distinction (§2), the world partition (§3), the tradeoff analysis against geohash and H3 (§4.9), and the prefix / viewport predicates including the ancestor rule (§6). None of this exists elsewhere.

Borrowed, and better left borrowed

Origin, transfer, tip and burn belong to BRC-159 and are now cited there, not restated. The command set and the ::: framing are BRC-503's. The coordinates.* convention is BRC-503 §5's. State resolution moved to BRC-514. An earlier draft restated all of these, and in three cases restated them differently — which is how the pairing rule came to contradict sat ordering.

Two layers: who may move it, and where it is

A location system sitting on top of an ownership system can easily collapse the two. §5 keeps them apart. Ownership answers who may move a thing. Position answers where it is and what a query returns. Only the second belongs to this BRC.

The diagram below shows the case where ownership comes from 1Sat: the object changes hands at every hop, and moves at only two of them. An indexer that reads a change of holder as a change of place has merged the layers.

CUSTODY — BRC-159 owner Aowner Bowner Cowner Dseq 0seq 1seq 2seq 3 POSITION — BRC-515 SET quadkey 0320…232inheritedSET quadkey 0320…301inherited Three transfers. Two relocations. The object sits at the same coordinates through seq 0–1 and again through seq 2–3.
Where the layers touch: under spend authority, writing MAP data means spending the output, so the ownership check and the position write happen in the same transaction. That is why §5 needs no signature scheme of its own — the model already binds the command to a party. See Where ownership comes from.

Where ownership comes from

The spec requires an authorization model and mandates none, naming two that already exist. That is deliberate: the asset decides. A 1Sat token is owned by whoever can spend it. A system with no tokens at all might identify users by AIP or Sigma signature and treat the signer as the owner. Either works, and anything else a system defines works too. What an indexer may not do is skip the check.

modela command is authorized when
Spend authority (1Sat)the transaction carrying it spends the object's current tip — so it is a link in that object's own chain
Identity signaturethe AIP or Sigma signature on the carrying transaction belongs to the party entitled to edit the target record
Why the spec had to say this out loud. An earlier draft asserted in prose that only the holder could move an object, and left it there. MAP's SELECT <txid> lets any transaction name any earlier one — that is what the command is for — and MAP has no idea who owns anything. §2.1 used exactly that grammar to unplace an object and never said which transaction was allowed to send it. Read literally, an indexer built to the spec would accept SELECT <someone-else's-txid> REMOVE quadkey from a stranger and drop their object off the map. An indexer built by the people who wrote it would not, because they knew what was meant. That gap is now an indexer MUST.

The fix stayed out of the ownership business. §5 requires that a model be in force, requires the indexer to say which one it enforces, and requires it to ignore commands it cannot authorize. Which model, and how it decides, is the asset's business.

The cell encoding, drawn

§4 specifies the quadkey in formulas and pseudocode. The formulas are correct (see Verification); they are just hard to hold in the head. These three figures are the same rules as pictures.

Digits are quadrants

One digit per zoom level, digit = x bit + 2 × y bit, with y growing south as in web tiles. The consequence worth internalising: 0 and 1 are the northern half, 2 and 3 the southern. That single fact is what makes the polar-cap rule below decodable on sight.

north 0 1 2 3 x bit 0 x bit 1 y bit 0 y bit 1 south — y grows downward

A prefix is an ancestor — which is the whole argument for quadkeys

Appending a digit descends a level; dropping one is the parent. So a region query is a string prefix match on any ordinary key/value index, with no spatial index and no geometry library. That property, plus decoding straight to the z/x/y pyramid every renderer already draws, is what §4.9 spends its comparison grid buying — and what it pays for at the poles.

0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3 zoom zoom zoom 0 03 032 0320 z=1 · 20 038 km z=2 · 10 019 km z=3 · 5 009 km z=4 · 2 505 km
The first four digits of the spec's own z=18 New York vector. Each prefix decoded with the §4.5 inverse and checked to contain the point — nesting confirmed empirically, not assumed.

One object, many cells

§2 makes quadkey list-valued, using MAP's own commands: SET places, ADD adds a cell, DELETE drops one, REMOVE clears them all. For anything linear or irregular — a road, a river, a border, a delivery zone — that removes a bad trade. The smallest single cell containing a road is enormous and mostly empty; minting separate objects loses the shared identity.

one cell — SET only many cells — SET + ADD smallest containing cell — 64× the area, mostly empty the eight cells it actually occupies
§6 handles this without change: a record matches if any of its cells matches, returned once. §4.7's guidance became “pick a precision, then name as many cells as the object occupies”.

The projection stops before the poles

Web Mercator maps the ellipsoid to a square that cuts off at ±85.0511287798°. §4.1 spells the consequence out, and the rule has a pleasant property: because the north cap collapses into tile row y = 0 and the south into y = n−1, a polar record is visible in its quadkey alone — all-{0,1} digits means north cap, all-{2,3} means south. Longitude still resolves inside the cap; latitude does not.

φ ≥ 85.0511° — outside the square +85.05° −85.05° y = 0 digits 0, 1 only y = n−1 digits 2, 3 only φ ≤ −85.0511° — outside the square
Grid drawn at z=3 for legibility; cap bands exaggerated — the real clip discards 4.95° of latitude at each pole. This is why §4.1 says polar content SHOULD carry coordinates.lat / coordinates.lng.

Verification — method and results

§4 is the part of BRC-515 that can be settled by computation, so it was. The method: implement §4.1–4.5 from the specification text alone, without consulting an existing quadkey library, then compare against every numeric claim the document makes. An independent implementation that reproduces the published vectors shows the prose is unambiguous enough to implement.

claimsourceresult
MAX_LAT value§4.1reproduced 85.0511287798066
Three test vectors§4.8reproduced 032010110301120232, 0320101103011202, 3 — exact
Bounding box of the z=18 vector§4.8reproduced matches the published figures to six decimals
Zoom → equator edge table, all 7 rows§4.7reproduced against 40075016.69 / 2^z
Polar cap digit restriction§4.1confirmed points at ±89° clamp to y=0 / y=n−1, longitude still resolving
Exact prefix nesting§4.6confirmed the NY point falls inside all six prefixes checked
The y formula§4.2equivalent the standard slippy-map formula in artanh form — algebraically identical, better conditioned near the equator
Two details worth keeping. §4.2's explicit edge-assignment rule (“a point on an east or south tile edge goes to the eastern / southern neighbour”) is load-bearing: it is why the (0, 0) vector at z=1 correctly yields 3 rather than 0. Implementations that omit it will pass the New York vectors and fail on the equator and prime meridian. And the artanh form of the y formula is a deliberate improvement over the more commonly published tan/sec version, not a transcription error.

The prefix decode — every row computed, every row containing the point:

prefixztile (x, y)bbox lonbbox latequator edge
010, 0−180 … 00 … 85.051120 038 km
0321, 1−90 … 00 … 66.513310 019 km
03232, 3−90 … −450 … 40.97995 009 km
032044, 6−90 … −67.521.9430 … 40.97992 505 km
03201011030112021619288, 24645−74.0479 … −74.042440.6890 … 40.6931611 m
0320101103011202321877154, 98583−74.0451 … −74.043740.6890 … 40.6900153 m

Worlds, and what makes an id

§3 partitions the cell space: the same cell in two worlds is not the same place, and Earth is expressed by omitting the key entirely so the common case costs zero bytes. The part that needs care is what a non-Earth id may be.

same quadkey same quadkey no world key Earth — the default world = a3f9c1e4… seeded world — id is self-unique
World equality is the exact world bytes. It never consults MAP's app key — §2 says indexers MUST NOT even require app — so the id has to carry its own uniqueness.
Why the id has to self-uniquify. myapp:<seed> duplicates MAP's app key, yet dropping the prefix is unsafe: with world equality defined on world alone, a bare world=seed-7 silently merges every unrelated app shipping the same string. §3 resolves it by leading with a genesis outpoint, offering a hex hash for off-chain worlds, demoting myapp:<seed> to a fallback, and forbidding bare seeds outright.

Portals

§5 allows world to change on any authorized write. Because MAP state carries forward, a write that sets only world leaves quadkey in effect — the object arrives at the same cell in a different world. The object's identity is untouched — under spend authority, the BRC-159 origin — so identity survives the trip.

seq 0 — origin SET quadkey 0320…232 (no world key) Earth seq 1 — portal SET world a3f9c1e4… quadkey inherited seeded world — same cell seq 2 — return SELECT <txid> REMOVE world quadkey inherited Earth again spend spend Identity is unchanged throughout — it survives the portal.
Earth is expressed by omission, so the return trip clears the key with REMOVE world rather than overwriting it. SET world earth is honoured under §3's tolerance rule but SHOULD NOT be written in preference. An object holds many cells and exactly one world.

What a region query returns

§6 defines queries as predicates, which is the right altitude. The rule an implementer will skip is the third one: a viewport matches not only the cells inside it and the cells covering it, but its ancestors — otherwise a city-sized tag silently disappears the moment the user zooms to a street.

ancestor cell viewport
cells intersecting the box — match coarser ancestor tag — also matches viewport (west, south, east, north) ✗ outside — no match
Schematic. §6 also gives a sufficient algorithm using prefix lookups only, and requires antimeridian-crossing boxes to be split at ±180 and unioned — both easy to miss and both worth a conformance test.
Conformance vectors are in the spec. §6 ends with the three cases that separate a conforming implementation from a plausible one: an ancestor tag coarser than the viewport (matches), a box crossing the antimeridian (splits and unions), and a point exactly on a tile's east or south edge (lands in the neighbour, per §4.2).

What remains

Three open, one closed. None is a change to BRC-515 — each lives in a neighbouring repository or an unwritten document.

closedresolved R1 — MAP's two specs are now one

MAP carried two incompatible specifications on two branches, and the default branch was the older one:

branchcommandswhat DELETE meant
master (was default)SET, DELETERemoved a key/value mapping — the scalar removal command.
v2all sixRemoves values from a list. REMOVE takes the scalar role.

That disagreement is where the downstream confusion came from: go-templates and 1sat-sdk both shipped a DEL command that exists in neither spec, and an earlier draft of this BRC used DELETE for a scalar key — correct under master, wrong under v2.

Resolved. opldotdev/MAP PR #3 is merged; master now defines SET, REMOVE, ADD, DELETE, SELECT and CLEAR, with DELETE taking a key and then the values to strike. BRC-515 no longer cites a spec that contradicts its own default branch. The v2 rewrite had dropped the author credits master carried, so those were restored before the merge rather than lost to it.

blocking R2 — BRC-503 is not merged

BRC-515's primary citation is BRC-503, which exists only in opldotdev/BRCs PR #1 alongside the rest of the 500–513 corpus. Until that lands, the MAP dependency has no number, which is the original reason the draft restated MAP's command set in the first place. It is also why 515 was chosen: it clears the whole corpus and the contested 314. Land PR #1 first or alongside.

highin review R3 — MAP state along a 1Sat chain now has a spec

“The quadkey in effect at the tip” is only well defined if there is a rule for resolving a MAP key's value across a chain of outputs. MAP cannot supply it — it has no ownership model, and its only context mechanism (SELECT <txid>) addresses a transaction. BRC-160 declines it explicitly: it does not say which envelope is “the” content for an application — those rules “belong in application or content-resolution specs”.

Implementations resolve it anyway, and have for years: merge each MAP revision along the chain in sequence order, last write wins. 1sat-stack indexes rev:, map: and par: per origin per sequence and merges forward.

Done: drafted as BRC-514, MAP State Resolution over a 1Sat Chain (PR #4, with its own companion). It defines the revision order, the state model, and how each of MAP's six commands folds — It also raises the question nothing had answered — whether a bare, in-chain REMOVE needs a SELECT at all — and leaves it open for a reviewer (Q4 on the 514 companion) rather than settling it. BRC-515 §5 now cites it rather than carrying an interim rule. Open until PR #4 merges.

highmerged R4 — two implementations carried the same MAP parser defect

Verified against current code in both repositories, not module caches.

repofilestate
go-templatestemplate/bitcom/map.goDeclares SET, DEL, ADD, SELECT. Implements SET only.
1sat-sdkpackages/templates/src/bitcom/map.tsSame four in a MAPCommand enum — a direct port, same DEL.

Neither DEL nor the missing REMOVE/CLEAR matches either MAP branch. The shared defects:

defectdetail
DEL is not a MAP commandIt appears in no MAP branch and in no other implementation. go-map uses "DELETE"/"REMOVE"; bmap switches on both.
Dead constantsIn go-templates, MapCmdDel, MapCmdAdd and MapCmdSelect are referenced by nothing — not even tests. Only SET reaches a code path.
Silent empty decodeA non-SET command returns a non-nil result with an empty data map. Callers treating non-nil as “decoded” lose data without an error.
No ::: handlingZero occurrences in go-templates. A ::: push is swallowed as a key or a value, so a second command in one output corrupts the first.
No list valuesAdds []string (Go) and adds?: string[] (TS) are declared and never populated. Multi-cell placement cannot be parsed at all.
Wrong protocol name1sat-sdk map.ts says “MAP (Metadata and Payload) Protocol”. It is the Magic Attribute Protocol — the SDK's own constants.ts has it right.
Done: both repos corrected to the canonical command set, with no alias for DEL — nothing on chain was ever written as it, so there was no migration. go-templates#4 also implemented the missing decoders and made an unrecognised command return nil rather than a silently empty record; 1sat-sdk#26 fixed the writers, which were building DELETE output that could never be decoded back. The ::: separator is deliberately still unhandled and is tracked in 1sat-sdk#28.

Also noticed, not a defect: 1sat-sdk's BSocialContext already uses geohash as a context key. BRC-515 declares geohash as a non-placing read key derived from the same point, so the two uses are compatible — worth confirming the value grammar matches when both land.

What BRC-515 is now. It adds no new envelope, no command set and no ownership model. Its cell-system choice arrives with a decision grid and an honest accounting of what that choice costs at the poles. The test vectors run — I checked every one. Queries are defined as predicates, and the scope line (“tags are claims, not proof of presence”) forecloses a whole class of misreading. §5 requires an authorization model without mandating one, names two that exist, defines position, and cites BRC-514 for state resolution. What remains is a tight, checkable key profile whose open items all sit outside it.