NDE alignment
- Linked Open Limburg is positioned as a service platform, in line with the NDE report Van data naar dienst.
- Linked Open Limburg is built on the NDE Stack, the ecosystem of NDE-compatible components.
- The search pipeline selects datasets dynamically from the Dataset Register.
- Datasets are only accepted into Linked Open Limburg if they are published according to the NDE Schema.org Application Profile, the shared baseline for cross-collection discovery. The profile permits a range – a creator is preferably an authority record but MAY be named inline – so a conformant dataset that fills few fields is admissible, and simply contributes nothing to the facets it has no values for. Acceptance is not the same as conformance in every field, though – see Deviations.
- Linked Open Limburg software components and applications are open source and adhere to the NDE Software Requirements.
- For hosting the software applications, Linked Open Limburg follows NDE’s infrastructural approach.
Deviations
Accepted datasets do not satisfy the profile in every field, and the index does not discard what falls short. Each entry below has two halves:
- The deviation is the publisher’s – a value the profile does not admit. We report these; we do not invent them.
- The compensation is ours – what the schema does with that value instead of emptying the field. What we index is conformant, since the deviation is in the input rather than the output. But a compensation is a claim we make that the publisher did not, which is why each one is written down here.
The two halves do not end together. Once the profile admits a structured size, publishers stop deviating – and we go on reading the number out of the node, because that is where it is.
Additions are neither, and are not listed: hasMedia, type, iiifManifest and dataset are index affordances no profile property backs, and they add rather than paper over. An addition can still acquire a compensation, and iiifManifest has one: the field is ours to define, but which property it reads when a publisher follows an older version of the profile is listed below.
dateCreated – an interval is collapsed to its start
The profile requires a single date at the highest precision known, and forbids an interval “even when the exact creation date is uncertain”. The reason is typing, not taste:
Unlike
schema:temporalCoverage,schema:dateCreatedranges overschema:Dateandschema:DateTimeonly, neither of which permits an ISO 8601 interval; intervals belong toschema:temporalCoverage, which describes the content’s subject period rather than its creation.
Publishers ship intervals anyway – 466 values across 31 datasets in the register, and in Limburgs Museum the majority: 252 of 387 dated works.
We index the start bound, so a work made somewhere in 1935–1955 is dated, sorted and filtered as 1935. That reads as more certain than it is, and for 244 works the indexed value is our interpretation rather than the publisher’s assertion – the costliest compensation here.
The profile’s text and its worked example disagree, and we follow the example, which resolves 1980/1985 to 1980 – the start bound, not the “lowest common precision” the text prescribes. That operation cannot be written for almost any work needing it: the legal forms are YYYY, YYYY-MM and YYYY-MM-DD, so two bounds share an expressible precision only inside one year (8 of the 252), and neither a decade nor a century can be said at all.
Both readings conform – 1935 and an absent value are equally well-typed – so this is a choice about what to assert, not about compliance. NDE’s own Linked Art mapping makes the opposite one at the same wall, dropping a production span that “straddles more than one year”; on this collection that discards 244 of 252 works. A consumer comparing the two indexes sees different dates for the same object, and neither is wrong.
Deletable when the profile can express a production period. Raised at issue 90; the range argument rules out permitting intervals or EDTF on dateCreated itself, so the answer is a modelling change rather than a syntax one – see schema-profile#177 for the rationale and #192 for EDTF on temporalCoverage, raised from this project.
creator – a Role wrapper is read through to the agent
The profile forbids the wrapper outright. § 4.2.7: multiple creators “MUST each be expressed as separate, flat creator values”, and creator “MUST NOT wrap its values in a Role node, since this would force consumers to handle two value shapes”. Publishers who want to qualify a role are pointed at schema:contributor, which the profile puts outside its own scope.
Publishers wrap anyway, and it is the majority shape by a wide margin. Of the register’s SPARQL-served datasets, 29 wrap creators in a Role (1 559 473 works) against 12 that state them flat (18 865) – 98.8% of creator-bearing works take the forbidden form. It is two platforms rather than one publisher: 28 LDMax/Axiell datasets and Beeld & Geluid.
work schema:creator [ a schema:Role ; schema:name "uitgever" ;
schema:creator <…/ark:/61567/…> ] .Unrepaired it empties both creator fields and puts a job title in one of them. creator holds the Role node – a blank node for 159 of Limburgs Museum’s 291 values and for all 17 of Discovery Museum’s, so no facet key at all – and creatorName holds the Role’s own name, which is the role. All 179 of Limburgs Museum’s creator-bearing works and all 15 of Discovery Museum’s answer “who made this?” with fotograaf, uitgever or onbekend.
We read the agent behind the Role, so creator keys on a person and creatorName names one: 181 distinct agents at Limburgs Museum, 15 at Discovery Museum, every one IRI-identified. This is the compensation with the least interpretation in it – the publisher did state a creator, in a shape the profile spells out and rejects, and we read the value they put there.
The wrapper is found by its second creator hop, not by the node’s type. Publishers vary in everything else: Limburgs Museum names the role on schema:name, Beeld & Geluid on schema:roleName (sometimes a Wikidata IRI rather than a string), and Beeld & Geluid’s wrapped contributors are typed schema:PerformanceRole, a Role subclass no store here reasons over. The hop catches every variant and cannot fire on a conforming publisher, because a Person is not the creator of anything.
The role statement is kept, not discarded. The transform rewrites only what creator and creatorName read; the Role and its name stay in the stream. fotograaf is the one genuinely qualified thing in this data, and the profile’s own answer for it is contributor – see issue 136.
Deletable when publishers state creators flat. Unlike the compensations below, this one has a clear end: the profile already forbids the shape, so it is a matter of the two platforms catching up rather than of the profile changing.
sdDatePublished – filled from the record’s modification date
The profile requires it on every record. Across the register’s CreativeWork partitions, only 7 datasets declare it. Most record the same fact elsewhere: 38 under dcterms:modified and 11 under schema:dateModified, both record-level.
We fall back to both, preferring the profile’s own property wherever a publisher supplies it, then dcterms:modified, then schema:dateModified. Reading only the first fallback would leave those 11 datasets with nothing – and since the field is required, that does not fail loudly: every document is dropped and the run still reports success (ldelements/lde#738). required stays – it is GraphQLNonNull in the API, so relaxing it to admit one publisher would weaken the guarantee for every consumer.
For Limburgs Museum the fallback is total rather than partial: all 412 IRI-identified works carry dcterms:modified and none carries sdDatePublished.
A further 81 datasets record none of the three, and no fallback reaches them. Onboarding one is a decision about that dataset, not a schema change.
Deletable when publishers emit the profile property. Low risk meanwhile, since both properties describe the record, so the substituted value answers the question the field asks.
sameAs on a place – the aligned term becomes the key, and the description
The profile models an external term as an alignment, not as an identity. § 3.4: the DefinedTerm is “a local node, scoped to the publishing dataset”, the sameAs URI is “the canonical identifier”, and consumers MUST compare on it rather than on the nodes’ own identities, “which are local to each publishing dataset and therefore not shared across datasets”. § 4.2.14 adds that a referenced term used as a location needs Place in its @type, because schema:contentLocation takes Place, not DefinedTerm.
Publishers deviate two ways, counted over every node reached by locationCreated, contentLocation, location, birthPlace or deathPlace – 277 in Drapo, 19 in Limburgs Museum:
- Drapo conforms: 263 of its 277 are
DefinedTerm+Placecarrying asameAsinto GeoNames, and the remaining 14 are custom resources with no term available, which § 4.2.14 allows. - Limburgs Museum uses the canonical IRI as the local node’s own identity – 10 nodes whose
@idishttps://sws.geonames.org/…, with no local node and nosameAs. - One is a GTAA node in the same shape – its own IRI is the thesaurus term – so it is keyed and described the same way, from GTAA rather than GeoNames.
- Eleven of Limburgs Museum’s 19 are also typed both
schema:Placeandschema:Person, one IRI carrying a place and the people associated with it:…/2759794/is “Amsterdam” and a printer,…/2751306/is “Maas” and“onbekend”. Reported rather than compensated – see #94.
We key a place on the term it aligns to, and take its description from that term. Where a node’s alignment – or its own IRI – lies in a source LOL covers, the document is keyed on that IRI and its language-tagged names and coordinates come from the authority, read through the Network of Terms; the publisher’s statements about it are discarded rather than merged. Three reasons, in order of how much they cost:
- Publisher data cannot be trusted for a term the publisher merely referenced. Limburgs Museum’s names for these nodes include a printer’s name and
“onbekend”. Keeping them would put one of those on a place shared with another collection. - One document per place is the point. Six GeoNames IRIs are referenced by both datasets today; keyed locally they are six places twice over, in two facet buckets with two labels.
- Nothing else can supply the description. Neither publisher ships
schema:geoat all, and no address component appears on any place in either dump, so the coordinate a consumer asks for exists only at the authority.
Coverage is GeoNames, GTAA and Wikidata, listed in packages/search-schema/src/covered-sources.ts and decided there rather than against the Network of Terms’ own sources registry. Deciding it statically is an identity constraint, not caution: a registry read would make the document key depend on a request, so one outage would re-key every place, rewrite every reference to them, and let two runs over identical dumps disagree. The Network of Terms is the route to an authority, not the arbiter of which ones LOL keys on. Widening the list is a PIPELINE_VERSION rotation, since no publisher’s dump changes when it does.
GeoNames is preferred where a node aligns to several, because it is the only one carrying coordinates; GTAA and Wikidata carry a name and no coordinates, which is all keying needs.
No spelling is rewritten. Both collections already write every term IRI in its authority's canonical form – all 288 distinct GeoNames targets in Drapo and all 12 in Limburgs Museum as https://sws.geonames.org/N/, and Limburgs Museum's one GTAA term as http://data.beeldengeluid.nl/gtaa/N (measured 2026-08-25). There is nothing to reconcile, and a normalisation that has never run against real data is one nobody can be sure is right. Repetition costs nothing either: Drapo states those 288 targets over 554 triples, one per named graph, and the key rule dedupes. Should a publisher ever spell a term two ways, that is the point to add a rule – for the spelling actually seen.
A place with no alignment keeps the publisher’s identity and the publisher’s description, unchanged. LOL does no entity matching: “Kessel” appears in both datasets as a local place and stays two documents, because neither publisher said they are the same.
Deletable when publishers align their places to a covered vocabulary and describe them as § 3.4 asks – a local node carrying sameAs, rather than the canonical IRI standing in for one. The 10 Limburgs Museum nodes would then key the same way for a conforming reason, and the person conflation would go with them.
iiifManifest – the manifest is read from the property an older profile version put it under
The profile (§ 4.2.6) requires the IIIF Presentation manifest to be an associatedMedia entry of its own, with @id the manifest URI and encodingFormat application/ld+json;profile='http://iiif.io/api/presentation/3/context.json'.
Publishers ship the older shape – a version lag, not a modelling error, which makes this the cheapest deviation here to retire. Before v0.6.0 the profile put both IIIF APIs on the MediaObject under schema:isBasedOn, requiring an encodingFormat on each target and sh:pattern-constraining it to …/api/(image|presentation)/3/context.json. v0.6.0 (2026-03-04) moved the Presentation manifest to the work and narrowed isBasedOn to the Image API alone; v0.11.0 (2026-05-14) dropped it there too. Limburgs Museum and Discovery Museum still emit the pre-v0.6.0 shape, discriminator included.
We read the manifest from there, by the discriminator that profile version defined – the same encodingFormat test the conformant route applies, one hop further out. Nothing is inferred: the publisher states which API each target is, and the two collections do so on every reproduction (765 and 37, each with exactly one Presentation target). Without it the field was null for all 463 Limburgs Museum and 17 Discovery Museum works with media, while Drapo – which ships the conformant entry – answered normally and carries no isBasedOn at all.
A conformant entry always wins, so this only ever fills a field that would otherwise be absent. It is the mildest compensation listed here: the value, and the fact that it is a manifest, are both the publisher’s own assertions. What is ours is only the decision to keep reading a retired property.
Deletable when publishers emit the manifest as its own associatedMedia entry with the IIIF encodingFormat. Reported as finding 5 on issue 94; raised from issue 134.
size – the number is read out of a structured node
The profile currently types size as a display string. Publishers ship structured values too – 3246 of 3397 size violations across 32 datasets are schema:QuantitativeValue nodes.
We read size/value as an alternative step, because a bare size path frames the node, finds no literal, and empties the field for every work in those datasets without a word. This is repair rather than circumvention: the live proposal keeps the display statement unchanged and additionally allows a QuantitativeValue per measured dimension, so the shape we read is the one the profile is moving towards.
The unit stays out of reach, for a narrower reason than the profile having nowhere to put it: the proposal asks for a unitCode URI and a valueReference naming the dimension, while published nodes carry unitText and an untagged name such as "diepte". Separate width/height/depth properties were proposed and rejected – Schema.org puts them on VisualArtwork, and adoption was below the profile’s bar.
Deletable when publishers emit units the way the profile asks. The proposal landing will not do it: it sets a target rather than blessing current data, so the alternative step outlives it.