CoolFace
Datasetpublic

yuiseki/geo-triples-japan

geo-triples-japan 510,616 spatial triples, 483,922 text rows and 8,890 evaluation questions, computed from two frozen, openly-licensed sources by an oracle with no model and no network in the loop. Same input and same versions, same Parquet, byte for byte. Three things are kept apart throughout, in the data and in this card. YuisekinGeoSPARQL observes: it reads a DE-9IM matrix off two published geometries. LeanGeospatial certifies: it proves what a matrix entails and what the… See the full description on the dataset page: https://huggingface.co/datasets/yuiseki/geo-triples-japan.

sourceHugging Faceodblupdated 9h agoView on Hugging Face
0likes
Dataset Card

geo-triples-japan

510,616 spatial triples, 483,922 text rows and 8,890 evaluation questions, computed from two frozen, openly-licensed sources by an oracle with no model and no network in the loop. Same input and same versions, same Parquet, byte for byte.

Three things are kept apart throughout, in the data and in this card. YuisekinGeoSPARQL observes: it reads a DE-9IM matrix off two published geometries. LeanGeospatial certifies: it proves what a matrix entails and what the RCC8 composition table allows. This repository derives: it turns the two into triples and sentences. A row says which of the three it is, and never borrows one's confidence for another.

Nothing here was generated by a language model. Every sentence is a template from src/vocab.py filled with two names, and which template applies is decided by a DE-9IM matrix computed from geometry.

python
from datasets import load_dataset

triples = load_dataset("yuiseki/geo-triples-japan", "triples", split="train")
cpt     = load_dataset("yuiseki/geo-triples-japan", "cpt", split="train")
probe   = load_dataset("yuiseki/geo-triples-japan", "probe", split="train")

Where it comes from

layerfeaturessourcelicence
jp-country1`yuiseki/osm-japan-src-2026-08` b51be03ODbL-1.0
jp-pref47the same datasetODbL-1.0
jp-muni1,740the same datasetODbL-1.0
jp-poi80,748the same dataset, point and polygon tablesODbL-1.0

One source, four layers, all cut from the OpenStreetMap planet file of 2026-08-31. The administrative layers are admin_level 2, 4 and 7: prefectures, and then municipalities, where OpenStreetMap puts the special wards of Tokyo at the same level as a city.

jp-poi is one feature per Wikidata id, from the point and the polygon tables, keeping the area where a place is mapped both ways. Any id that an administrative boundary in the extract carries is dropped, because those are their own layers and would otherwise arrive twice under different IRIs.

82,536 features make 6,812,108,760 ordered pairs. 284,198,196 of them are formed and 205,426 of those are not disjoint. The places are compared against the municipalities and against nothing else: 80,748 places against each other is a different dataset with a different cost, and the question they were added for is which municipality a place is in.

A pair that was formed and is absent is disjoint, and how that is spelled depends on the kinds: FF2FF1212 for two areas, FF0FFF212 for a point against an area. A pair that was never formed is absent for a different reason and says nothing. The oracle's manifest carries both the table and the list of which layers were compared.

The oracle is YuisekinGeoSPARQL, which loads the sources into Apache Jena Fuseki 6.2.0 and compares GEOS through shapely against JTS inside Jena, pair by pair, for all eight Simple Features predicates. Two implementations of one standard agreeing is evidence; one agreeing with itself is not.

The triples subset

3,286,232 rows: 2,053,684 true and 1,232,548 false.

One row per observed pair per predicate, so eight rows per pair, 1,643,408 in all. The predicates that hold are true and the rest are false, and the truth of all eight comes from one matrix, which is in the de9im column of every row. Negatives are chosen this way so that no sampling rule can be argued with: nothing was selected, and a reader who disagrees with a false has the evidence in the same row.

More than one Simple Features predicate holds of a pair. Two equal areas are sfEquals, sfIntersects, sfWithin and sfContains at once. The exclusive reading is the rcc8 column beside them, where exactly one of eight relations holds.

Not for every pair, though. RCC8 is a calculus of regions, and a place mapped as a node is a point, so 74,156 observed pairs have an empty rcc8 and their Simple Features columns are the whole reading. The kinds are columns for the same reason: sfOverlaps needs both operands to have the same dimension, and sfCrosses needs them to differ in a fixed argument order.

column
subject_id, subject_iri, subject_name, subject_source, subject_layerthe subject feature
predicateone of the eight GeoSPARQL Simple Features predicates
object_id, object_iri, object_name, object_source, object_layerthe object feature
truthwhether it holds
de9imthe nine-cell matrix that decides it
rcc8the single RCC8 relation of the pair, empty unless both kinds are area
subject_kind, object_kindpoint or area
derivationobserved, read off the geometry, or composition, entailed by two observed rows
certificationcertified if LeanGeospatial proves this step, uncertified if it leaves it open
certificatewhat was proved: de9im:entailed, de9im:refuted, or the composition cell
via_id, via_irithe intermediate feature, for composition
outside_ratiofor an observed pair, how much of the subject lies outside the object; empty for a composed row, which was never measured
source_dataset, engine_version, schema_versionprovenance, on every row

What is proved, and what is only observed

All 3,286,232 rows are certified, and it is worth being exact about what that does and does not cover.

What is proved is the step from a matrix to a predicate. Given matrix FF2F11212 over two areas, LeanGeospatial derives sfTouches and refutes the other seven; the verdicts for all 25 matrices this data produces, 200 in all, are vendored in vendor/de9im_sf_verdicts.tsv with the revision that produced them. The build reads that file and stops if a verdict disagrees with what the oracle read. None does. For a composed row, what is proved is the cell: EC x NTPPi entails DC and nothing else, in the table table_eq_published shows equal to the machine-checked one.

The kinds are part of the question. sfCrosses between an area and a point is not one of the cases SFA lists, and the prover refuses it even though the matrix of a ward holding a point matches the pattern perfectly well. Asking without the kinds would have shipped every ward crossing every place inside it.

What is not proved is the matrix itself. That two geometries relate as FF2F11212 is a measurement made by GEOS, checked against JTS, on vertices that OpenStreetMap published. No theorem covers it, and the certification column does not claim otherwise. Nor is the RCC8 label of a composed row's premises proved: those are observations that the proved cell was applied to.

Lean is not a dependency of the build. Regenerating the verdicts needs it; reading them does not, which is why they are a vendored file rather than a step.

True triples, by predicate

predicateobservedcomposedtotal
sfDisjoint01,071,7881,071,788
sfContains105,076271,470376,546
sfWithin105,076271,470376,546
sfIntersects205,4260205,426
sfTouches16,08027616,356
sfOverlaps7,01447,018
sfEquals404
sfCrosses000

This is lopsided and the shape is worth reading before using it.

sfDisjoint is 52% of all true triples and every single one is a deduction. No observed row is disjoint, because the oracle only writes pairs that are not. sfCrosses is never true, because SFA gives it no area/area case at all and every feature here is an area; it stays in the table as eight thousand honest negatives rather than being dropped.

The rcc8 column over the 205,426 observed pairs: NTPP 50,736, NTPPi 50,736, EC 16,080, PO 7,014, TPP 3,350, TPPi 3,350, EQ 4, and 74,156 with none because a point is not a region.

The four EQ pairs are two places whose geometry is exactly a municipality's, because the same ways draw both: 壱岐市 and 壱岐国, 対馬市 and 対馬国, each a modern municipality and the historical province it replaced.

Composed rows, and which cells they use

1,642,824 rows have derivation = composition, almost exactly half the table. They are not extra observations: for each, two known relations through the feature in via_id left the composition table with exactly one possible relation, and that is what the row records. Cells where the table leaves several entail nothing and produce no row. At most one row is kept per ordered pair, since a pair reachable through many intermediates would otherwise repeat itself.

Sixteen of the 64 cells are used, against four in the Tokyo dataset. A hierarchy four layers deep exercises far more of the table than 23 wards did.

cellrowsentails
EC x NTPPi535,894DC
NTPP x EC535,894DC
NTPP x NTPP210,960NTPP
NTPPi x NTPPi210,960NTPPi
NTPP x TPP72,723NTPP
TPPi x NTPPi72,723NTPPi
NTPPi x TPPi1,714NTPPi
TPP x NTPP1,714NTPP
NTPP x EQ93NTPP
EQ x NTPPi93NTPPi
EC x EQ14EC
EQ x EC14EC
EQ x TPP12TPP
TPPi x EQ12TPPi
EQ x PO2PO
PO x EQ2PO

65% of them are one shape of argument: a municipality borders another municipality, that municipality contains a place, therefore the first municipality and that place do not meet. The NTPP cells are the interesting ones and they are 29% of the table: a place is in a municipality, the municipality is in a prefecture, therefore the place is in that prefecture, and again for the prefecture in the country.

762,114 rows are conclusions about pairs the oracle never formed. The places are compared against the municipalities only, so nothing measured 金閣寺 against 京都府; the composition table settles it anyway, and those rows carry an empty de9im rather than a matrix nobody read. They are the rows that reach past what was measured, which is what a composition table is for.

The composition table is vendored from LeanGeospatial, whose theorem table_eq_published shows its machine-checked table agrees with the published one on all 64 cells. The build stops if any composed row contradicts what the oracle observed. None does.

The cpt subset

4,678,910 rows, 1,011,429,399 characters. One row per true triple per form.

The text is layered, and each layer takes the triple as its input:

a triple -> an N-Triples line, for every true triple -> a sentence, only where both features have a label in that language

`form`rowscharactersmean length
ntriples2,053,684836,174,682407.2
ja1,848,01092,417,82850.0
en777,21682,836,889106.6

A holdout column marks the rows that name a held-out place, and a topic column says whether a row is about the administrative hierarchy alone or about the places inside it. A training run filters on both; everything else reads the table whole.

`topic``ja``en``ntriples`
admin39,12639,12658,246
place1,808,884738,0901,995,438

A pair column says which two layers a row is about, jp-muni>jp-pref and so on, so a run can take one rung of the hierarchy. It is there because the first attempt to train on admin learnt the majority sentence instead of the facts, and the majority sentence is not what anyone wants:

predicaterowscharacters
sfDisjoint17,780834,712 (70.7%)
sfTouches12,060174,736
sfContains4,64078,823
sfWithin4,64092,555 (7.8%)

Seven tenths of the Japanese admin text is "A and B do not meet", because every composed disjointness is one sentence and there are a great many pairs that do not meet. A model trained on it answered 接していない to everything, which is what the corpus mostly says. Within admin, the 1,740 facts that answer "which prefecture" are 3,480 rows and 42,908 characters once the country rung and the other predicates are filtered out, and they are reached with pair and predicate together.

admin is the whole hierarchy without the things inside it: 47 prefectures, 1,740 municipalities, which of them border which, and the country. 1,180,982 characters of Japanese, about 850,000 tokens. It is there because "can a 0.6B model hold 1,740 municipalities and their prefectures" is a question worth answering before "can it hold every shrine in Japan", and the two corpora differ by two orders of magnitude.

The N-Triples form is unconditional, which is the point: the coverage gap is a missing ja row beside a present ntriples row, visible by counting, rather than a choice made silently during the build. The IRIs are the ones the oracle's published graphs use.

<https://yuiseki.net/geosparql/jp-poi/poi-Q1187933> <http://www.opengis.net/ont/geosparql#sfWithin> <https://yuiseki.net/geosparql/jp-muni/muni-3684554> .
Kinkaku-ji is within Kyoto.
金閣寺は京都市に含まれる。

A composed row carries all three statements, in all three forms, because a conclusion on its own is indistinguishable from one more observation:

桜天神社は名古屋市に含まれる。名古屋市は豊山町と接している。したがって桜天神社と豊山町は接していない。
結縁寺は掛川市に含まれる。掛川市は袋井市と接している。したがって結縁寺と袋井市は接していない。

Why the three forms do not have the same number of rows

2,053,684 N-Triples lines, 1,848,010 Japanese sentences, 777,216 English ones.

205,674 true triples have no sentence in any language. 205,426 of them are sfIntersects, which holds of every observed pair and says almost nothing; along with sfCrosses it is kept in the triples table and in the N-Triples form and is never spoken, because a corpus full of "A and B intersect" teaches a model to produce filler. The remaining 248 are a point and an area in a relation this vocabulary has no wording for.

That accounts for the Japanese gap exactly: it is zero. Every feature here is in Japan and carries a Japanese name, so every true triple that can be spoken at all is spoken in Japanese.

The English gap is 1,070,794, and it is a fact about OpenStreetMap rather than about this build: name:en is optional and most mappers do not write it. A feature with no label in a language gets no sentence in that language rather than a romanisation passed off as that language, which is why Japanese is 2.4 times the English half here.

How a place is worded

A point is not contained in a ward the way a smaller area is, so the two predicates that can hold between a point and an area have wording of their own.

厄神は加古川市にある。              Yakujin is in Kakogawa.
加古川市には厄神がある。            Kakogawa has Yakujin.
金閣寺は京都市に含まれる。          Kinkaku-ji is within Kyoto.

The third is an area, so it keeps the area wording. Everything else involving a point is left unspoken rather than worded by analogy: a point on a municipal boundary does touch it, and saying so takes a sentence about boundaries that nobody asked for.

Three things the sentences will say that read oddly

Some of the places are bigger than the municipalities. 79 features in jp-poi contain a whole municipality, and one of them contains all 1,740: 陸上自衛隊 方面隊, the army district. 本州 contains 843 of them, and the regions 関東地方, 中部地方, 九州地方 and 東北地方 contain a few hundred each. They are in the layer because they carry a Wikidata id and are not administrative boundaries, which is the only rule the layer has. What they say is true; it is just not what "place" suggests. Filtering on the direction of sfWithin removes them, and the probe subset already does.

3,504 places overlap a municipality without being inside it. A park or a campus that straddles a municipal border is PO, and the sentence says the two overlap without either containing the other. みどりが丘 府民スポーツ広場 lies across four municipalities of Kyoto.

Four pairs are exactly equal. 壱岐市 and 壱岐国, 対馬市 and 対馬国: a modern municipality and the historical province it replaced, drawn from the same ways. The N-Triples form distinguishes them by IRI; the sentence says they cover the same area, which is what the geometry says.

The probe subset

68,025 questions, every one of them answerable in Japanese and 29,005 in English. Which parent does this place have, at each level of the hierarchy.

levelquestionschoosing amongchance
place-in-municipality66,5411,740 municipalities0.06%
municipality-in-prefecture1,48447 prefectures2.1%

The second level is the question this whole line of work started from. A small model cannot answer "which prefecture is Matsuyama in", and a fine-tune cannot teach it: fine-tuning teaches the shape of an answer, and a model that has learnt the shape without the geography writes a well-formed place that does not exist. Until this dataset that question was the generalisation probe, asked about places no corpus here mentioned. Now a corpus states it, which changes what a score on it means, and the next paragraph says how.

The split, and what it can and cannot show

One place in ten is held out, chosen by the sha256 of its own id. Every cpt row that names a held-out place is marked holdout, in any position, so a training run that filters on that column has never seen the place at all. 408,356 rows, 8.7% of the corpus, name one of the 8,121 held-out places.

leveltraineval
place-in-municipality59,8856,656
municipality-in-prefecture1,4840

A question is eval when either of its two features is held out, not only its subject: a parent held out and a child not would be a question whose fact was never trained, counted as recall.

The municipality level is entirely train. The split is over places only, because holding out a municipality takes every sentence about every place inside it, and that cost 23.9% of the corpus to buy 142 questions.

The train half is a recall measurement. Every answer in it is stated in the part of cpt a run trains on. A rise says the facts went in, and nothing more than that.

The eval half is a control, not a hope. A place that never appears in training cannot be recalled from it, and nobody should expect it to be. Two things can still move it, and both are worth knowing. A model may learn the naming rule, since 39% of these places carry their municipality inside their own name, and that rule transfers to places it has never seen. And a model that has merely learnt to answer with a plausible municipality moves both halves together, which is how this half earns its keep: it is what says a rise in the other one is the facts going in rather than the shape of an answer.

The four numbers to read are therefore train and eval, each with and without the name leak. Only one of them is expected to move.

jp-pref inside jp-country is in the triples and not in the probe. There is one country in this graph, so "which country is 東京都 in" has one possible answer and measures nothing.

The parent is what the child is within, and that is the opposite of what the Tokyo dataset does. There, Natural Earth's admin-0 and admin-1 draw one coastline twice and disagree, so only 21 of Japan's 47 prefectures are geometrically within Japan and the parent has to be read off mere intersection. Here every layer is built from the same OpenStreetMap ways, so within is exact: all 1,740 municipalities are within exactly one prefecture.

Copying the Tokyo rule here would have been a quiet disaster. A municipality on a prefectural border shares boundary ways with the prefecture next door, so it touches it, and touching is intersecting: 781 of the 1,740 meet two prefectures or more. The set would have kept the inland municipalities and thrown away every border one in the country.

A child that is within two parents is dropped, and so is a child whose name does not pick it out. 130 municipality names are used by more than one municipality, so "which prefecture is 府中市 in" has two answers and any one of them scores a model on a coin toss; that rule is why 1,484 municipalities are asked about and not 1,740. A question whose subject has its answer's name is dropped too.

The answer is often inside the question

26,236 of the Japanese questions and 6,466 of the English ones have the answer in the subject's own name. "Which municipality is 市立福島第三小学校 in" carries 福島市 inside it, and a model that reads it off is not recalling anything this corpus taught. Two columns mark them, answer_in_child_ja and answer_in_child_en, so a score can be reported split rather than quietly inflated.

It is not a small correction. 120 questions per level with three worked examples, asked as a completion:

model`place-in-municipality`with the leak removed`municipality-in-prefecture`with the leak removed
Qwen3.6-35B-A3Bja43.3%9.2%90.0%86.7%
Qwen3.6-35B-A3Ben30.8%16.7%68.3%76.7%
Qwen3-0.6B-Baseja30.8%0.8%6.7%3.3%
Qwen3-0.6B-Baseen15.8%0.8%1.7%3.3%

Three quarters of the 35B's apparent score at the place level was reading the name. What is left, 9.2% in Japanese against a chance rate of 0.06%, is real and is a long way from saturated. The 0.6B base model is at chance once the leak is gone, at both levels and in both languages, which is the floor this corpus exists to lift.

The prefecture level barely moves, because a municipality is rarely named after its prefecture. 86.7% at 35B in Japanese is the ceiling to beat there.

Japanese beats English at both levels, which is the other side of the name:en gap: most of these places have no English name, and the ones that do are the famous ones.

The protocol matters more than it looks. Asked cold, a base model continues the question instead of answering it; wrapped in the chat template it ships with, it does the same. Both read as 0% and neither is about geography.

What this is for, and what is not known about it

The target it was built against is a model that cannot answer "which prefecture is Matsuyama in". Small models do not carry administrative geography, and a fine-tune teaches the shape of an answer rather than the geography, so the model writes a well-formed place that does not exist.

This dataset states that question's answer, and 66,540 harder ones like it: 80,748 named places, each in exactly one municipality, in a corpus that says so in three forms. Qwen3-0.6B-Base is at chance on both levels once the name leak is removed, so there is a floor to lift and a measured ceiling to lift it towards.

N-Triples text teaches IRI patterns and the shape of a GeoSPARQL statement. Whether that transfers to a Japanese question asked in ordinary words is an open question. It has not been measured here, and the card would rather say so than imply either answer. That is also why all three forms sit in one table with a form column: they can be weighted, or one of them dropped, without rebuilding anything.

Reproducing it

The code that builds this is at yuiseki/geo-triples-japan.

bash
git clone https://github.com/yuiseki/geo-triples-japan
git clone https://github.com/yuiseki/YuisekinGeoSPARQL
cd YuisekinGeoSPARQL
docker compose run --rm builder --out /data/jp \
    --source jp-country --source jp-pref --source jp-muni --source jp-poi

cd ../geo-triples-japan
python3 src/build.py \
    --relations ../YuisekinGeoSPARQL/data/jp/relations.tsv \
    --oracle    ../YuisekinGeoSPARQL/data/jp/manifest.json \
    --out data
python3 -m pytest

The oracle step is the long one: 44 minutes, of which the pairwise geometry is most. It peaks at about 6 GB, and docker-compose.yml caps the builder because this is a country and not a city.

data/manifest.json records the digests:

filesha256
triples.parquet166306db21e522d31f253e1cbd3608d3147eab7e8e2a4afc17ee53c25f7babe2
cpt.parquet919a7ebf572f8c50f34a9501c108b2a0b9cc6edbf1d75f6134e31b18def01796
probe.parqueta79915187f853fc6d737a5c91d34b565298e168ad30d227bf361ea1a61048a67

The oracle is run without --normalize snap. That flag adds three columns this build does not read, and nothing else, but it changes the digest of relations.tsv that the manifest records as the input.

Two builds of the same input produce identical bytes, and the test suite demonstrates it by running the build twice in separate processes, one of them with a randomised hash seed, and comparing. Parquet bytes belong to the writer as much as to the data, so the digests above hold for pyarrow 20.0.0 with zstd, which the manifest also records. The build refuses to run if the oracle's manifest does not describe the files on disk.

Prover requests, and regenerating the certificates

src/prover_requests.py turns the oracle's relations.tsv into the JSON Lines that LeanGeospatial's prover reads, so a composition claim can be checked against a machine-checked table rather than against this repository.

bash
python3 src/prover_requests.py \
    --relations ../YuisekinGeoSPARQL/data/jp/relations.tsv \
    --out data/prover/triples.jsonl \
    --claims-out data/prover/claims.jsonl --limit 0

lean-geospatial-prover < data/prover/triples.jsonl > verdicts.jsonl

src/certify.py does the claims half of this and keeps the answers:

bash
python3 src/certify.py \
    --relations ../YuisekinGeoSPARQL/data/jp/relations.tsv \
    --prover ../LeanGeospatial/.lake/build/bin/lean-geospatial-prover \
    --lean ../LeanGeospatial

It rewrites vendor/de9im_sf_verdicts.tsv, which is what fills the certification column, and refuses to write a file whose verdicts contradict the oracle.

200 distinct matrix-and-kinds-and-claim pairs, from 25 matrices. These files are not published as part of the dataset; they are a check, and they are large.

Licence

ODbL-1.0. The sentences too.

Every source here is OpenStreetMap, so there is nothing to combine: the whole thing is ODbL. A sentence is an extraction from OpenStreetMap geometry, not a description of it, so a corpus containing these sentences is a Derivative Database and carries ODbL too.

(c) OpenStreetMap contributors, available under the Open Database License. https://www.openstreetmap.org/copyright

See ATTRIBUTION.md, which sets out the ODbL position in full and says why it matters when mixing sources. The code in src/ and tests/ is MIT.