Skip to content

Name resolution

Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: not yet written.

Polish-aware name resolution is unconditional core in Nerthus.Core. A session mentions Erastera, Tussalowi, or Ithanowi; the resolver maps those inflected forms back to the canonical entities Eraster, Lord Tussal, and Ithan. PU awards, session distribution, and every @tag cross-reference depend on that mapping. Polish is the only language — tags, types, and names are Polish literals in canonical diacritical form (see Entities). The resolver runs in the daemon against an in-memory name index, with the grammar compiled into C# (Nerthus.DeclensionEngine, Nerthus.BKTree), plus one stage that reads the corpus's own annotation. This page specifies the seven-stage resolver, the name index that backs it, and the -Type / -ActiveOn / -Within disambiguation contract.

Concepts

  • Token — a single indexed name string: a full name ("Lord Tussal"), a registered @alias, a @slug, a @nazwa_nerthus, a @base_name, a @generyczne_nazwy value, or an individual word split out of a multi-word full name ("Lord", "Tussal").
  • Query — the surface form found in a session body or passed to Resolve-NerthusName. It is frequently inflected: Erastera, Ithanowi, Tussalem.
  • Stem — the suffix-stripped form of a token (IthanowiIthan), used as the bucket key in the StemIndex so declined queries collapse onto their lemma (the dictionary base form).
  • Owner — the entity a token resolves to; resolution returns the owner, never the token.
  • Homonym / Ambiguity — one token carried by several owners, e.g. the bare alias Gildia Teologów shared by the Thuzal and Ithan chapters after an operator split (Locations); the owner filter disambiguates.
  • Declension — Polish case inflection, handled by plain suffix stripping (Stage 2), consonant-alternation reversal (Stage 2b), and — for the multi-word forms neither reaches — the annotated lemma (Stage 2d).

The name index (data model)

Built by the daemon from the entity model, held in memory only:

{
  Index      : Dictionary[string, IndexEntry]    # OrdinalIgnoreCase — Stages 1, 2b, 3 keys
  StemIndex  : Dictionary[string, List[string]]  # OrdinalIgnoreCase — Stage 2 buckets (stem → tokens)
  BKTree     : Nerthus.BKTree                    # fuzzy metric tree — Stage 3
  Declension : Nerthus.DeclensionEngine          # the grammar (see below)
  Stats      : { tokenCount, stemCount }
}

Each IndexEntry (all fields always populated):

Field Type Meaning
Source string The token itself, as indexed
Owners list Every owner of this token, each a { Owner; Type; Priority } wrapper
Primary wrapper The collision-chosen default owner (see below)
Ambiguous bool True as soon as a second distinct owner claims the token

Token sources & priority

Every entity contributes at priority 1: its canonical name plus every @alias, @slug, @nazwa_nerthus, and @base_name value, and each comma-separated @generyczne_nazwy value. At priority 2: each ≥3-character word of a multi-word canonical name. So Lord Tussal indexes "Lord Tussal" (P1) plus "Lord" and "Tussal" (P2). The owner's type is its @typ value (falling back to the ## category heading of its index block).

Collision & the primary owner

A token the same entity claims twice deduplicates. A different entity claiming an existing token joins Owners and marks the entry Ambiguous — regardless of priority. Priority influences only which owner becomes Primary, the default pick when no filter disambiguates:

  1. Lower priority number wins (a full name beats another entity's word token).
  2. At equal priority: Postać beats Gracz, Lokacja beats Mapa.
  3. Otherwise the first-seen owner stays primary (stable).

The Postać-beats-Gracz rule is why the separate Gracz and Postać entity types (linked by @należy_do, see Players) coexist without a token war: a shared name defaults to the in-fiction character, and -Type Gracz still reaches the roster entry.

What feeds the index

Nerthus.MarkdownScanner (compiled C#, single-pass line scanner) is the one Nerthus parser. It emits entity blocks (Category, Name, and a flat Tags list with raw temporal values — operator prose is skipped) and raw session blocks (header + verbatim body); the index builder consumes the entity blocks. There are no scan caches on disk — scanner output, like the index, lives only in daemon memory (Architecture).

Build and rebuild

The index is built with the entity model and rebuilt whole whenever the daemon's fingerprint self-heal detects a source change (Architecture). POST /v1/api/name-index/rebuild forces the same full model rebuild on demand and reports buildMs plus the index tokenCount / stemCount; use it to pre-warm before a bulk session import.

Nerthus.DeclensionEngine

A compiled C# class (lib/, namespace Nerthus), constructed once at daemon start from three parallel tables — the declension suffixes (longest-first), the alternation inflected endings, and their base replacements. It exposes exactly two methods, both EndsWith/Substring with OrdinalIgnoreCase:

Method Used by Behavior
GetStem(text) index build + Stage 2 Strips the first matching inflection suffix (input order = longest-first); returns the input unchanged if none matches or the stem would fall below 3 characters (text.Length > suffix.Length + 2).
GetAlternationCandidates(text) Stage 2b Returns 0..N base-form candidates by reversing consonant mutations; the same minimum-stem guard applies; empty array if none apply.

The suffix table, in engine order:

ego  emu  ymi  ych  owi  ami  ach  iem     ej  ym  em  om  ie     ą  ę  a  u  y  i  o

Bare -o is the Polish feminine vocative and also the nominative of a masculine name like Mato, and it is stripped on both sides: the index stems its own tokens through this table, so Mato buckets under Mat and the query Mata reaches it. Bare -e is not in this table and must not enter it — the two are not symmetric, because -e is a common nominative ending and stripping it would reduce whole names to their stems across every consumer. Get-NerthusAliasStem states that rule at its own call site, and Add-NerthusNormalizeConflicts keeps a separate 21-entry list which does carry -e, because grouping two worklist rows under one stem cannot damage a lookup. The two lists are pinned against each other in tests/Resolve.Tests.ps1.

The alternation pairs (inflected ending → base ending), in engine order:

dzie→da  ście→sta  dzi→da  rze→ra  dze→ga  ście→ść  le→ła  ce→ka  ni→ń  si→ś  zi→ź  ci→ć

Example: the locative Anwardzie triggers dzie→da and yields the candidate Anwarda — a candidate is accepted only on an actual index hit (Stage 2b below). -ście appears twice — both readings (-sta, -ść) are emitted as candidates.

The pipeline (behavior)

Resolve-NerthusName runs up to seven stages in order; each stage funnels its candidate token through the same -Type / -ActiveOn / -Within owner filter (below), and the first stage whose filtered candidate survives wins:

Query "Anwardzie"
  ├─ Stage 1   Exact O(1) dictionary lookup ........... miss
  ├─ Stage 1b  Computed alias (the `exact` tier) ...... miss (no proven row for this surface)
  ├─ Stage 2   Declension suffix-strip → StemIndex .... miss (stem "Anwardz" hits no bucket)
  ├─ Stage 2b  Consonant-alternation reversal ......... miss (-dzie → -da → "Anwarda", not indexed)
  ├─ Stage 2c  Title strip ............................ miss (no role word in front)
  ├─ Stage 2d  Annotated lemma (names.tsv) ............ miss (no row for this surface)
  └─ Stage 3   Levenshtein fuzzy (BKTree) ............. HIT  distance 3 → "Anward"
                                                         → Owner: Anward (Gracz)

Each result records the stage that matched (exact / computed / declension / alternation / title / lemma / fuzzy) and a confidence score: 1.0, 0.97, 0.95, 0.9, 0.88, 0.92, or 1 − distance/|query| respectively — so an operator can see why Anwardzie resolved to Anward.

Stage 1 — Exact lookup (O(1))

Case-insensitive dictionary hit against Index. Catches canonical names, aliases, slugs, and @nazwa_nerthus values verbatim: "Ithan", "Lord Tussal", "rezydencja-tussal" all land here.

Stage 1b — Computed alias (O(1))

A form the alias pass proved injective on the corpus — it maps to one block and provably cannot map to another — looked up in the store the daemon publishes beside its other caches and resolved as the block it names. Frycek → Fredrick Flumiene lands here when no @alias records it. The tier that produces those rows is specified with the normalization worklist. Since ruling H124.1 a row this tier proves exact with no rival does not stay here for long: the alias autopilot writes it as an (auto) alias line within half an hour, and the next index build answers it at Stage 1.

It sits after Stage 1, permanently. A surface the lore itself names must never be reinterpreted by a computation: a hand-written @alias is an operator's decision and this is a machine's. It sits before declension because it is an equality on a form no suffix rule reaches, and 0.97 says exactly that — below a name the lore wrote, above a name grammar reconstructed.

Three things bound it. Global rows only: a session-scoped row is injective only given a cast and a resolver call carries none, so those are dropped when the store is read. Exact on both legs: the query is matched by fold and the row's block is resolved as written, with no stemming or fuzzy on either side. And the store must describe this corpus — it carries the stamp it was built from, covering the archive, the entity model and the annotation digest alike, and a file whose stamp does not match is ignored entirely rather than read. A host with no store, or with one describing another corpus, resolves exactly as it did before this stage existed.

The stamp is checked on every read rather than captured once, because the corpus moves without this index being rebuilt: fetching a transcript moves the archive segment while the entity model sits still. Nothing republishes the store on a boot or a tick — a full-surface worklist read is its only writer — so a stale file persists until such a read notices, and restarting the daemon does not clear it.

Stage 2 — Declension suffix-strip → StemIndex (O(1))

For queries of ≥3 characters, GetStem(query) strips the first matching suffix and the result is looked up in the StemIndex, whose buckets were filled at index time by stemming every token once. Suffixes are tried longest-first so -ami is removed before -i; the 3-character minimum stem keeps short names intact.

Examples: Ithanowi → Ithan (bucket Ithan holds Ithan), Rezydencji → Rezydencj (bucket holds Rezydencja), Perrinie → Perrin (-ie stripped; bucket holds Perrin), query Tussalem → Tussal (bucket holds Tussal). Both sides must stem to the same key. A masculine -o lemma like Losso stems to itself (-o is not a listed suffix), so the instrumental Lossem → Loss misses the Losso bucket and falls through; Lossem actually resolves at Stage 3, distance 2 (a recorded design decision — see below).

Stage 2b — Consonant-alternation reversal (O(1) per candidate)

When a Polish suffix mutates the stem-final consonant, plain stripping cannot recover the lemma. GetAlternationCandidates(query) strips a known inflected ending and re-appends the corresponding base ending (table above), producing candidate lemmas that are each looked up directly in Index. A candidate may be wrong — the reversal is heuristic — so only an actual index hit accepts it; a miss falls through harmlessly. Anwardzie illustrates both halves: -dzie → -da produces Anwarda, which is not indexed, so the pipeline continues and Stage 3 finds Anward at distance 3.

Stage 2c — Title strip (O(1))

A role in front of a name is not part of the name, and the transcripts are full of both: Kapitan Bejkadus, Sir Layonel Lodowa Łuska. The stage strips a known title and looks the remainder up exactly. It has to change the string and the result has to land exactly, so a form carrying no title leaves untouched.

The hit is held to a person type (NPC, Postać, Gracz). A title says what the referent is, and Ambasador Elancji strips to a token this corpus indexes for an embassy building — without the type guard the stage answered a person's line with a house.

Stage 2d — Annotated lemma (O(1))

The corpus's own annotation, read from nerthus.lang/names.tsv — folded surface → lemma, label, attestations — published by Nerthus.Lang from the entity spans it writes. The query is folded, looked up, and the lemma is resolved as written.

This is the stage that reaches what a suffix table structurally cannot: multi-word declined forms. Fortu Eder → fort Eder, Doliny Yss → dolina Yss, Domu Schadzek → dom Schadzek. Measured on the development corpus, of 989 annotated surfaces whose lemma is a canonical entity name, 168 are reachable by none of the stages above and 112 of those are multi-word.

Three rules govern it:

  • After Stage 1, permanently. A surface the lore itself names is never reinterpreted by the annotation. Nerthus.Lang holds the same rule on the writing side and publishes no row for such a surface; Stage 1 running first is how this side holds it regardless.
  • Exact only. The lemma is resolved as written — no stemming it, no alternation on it, no fuzzy through it. A lemma that lands on nothing falls through. The tagger disagrees with the stemmer on 25 % of labelled surfaces and most of that is noise (M → metr), which is what a looser rule would import into resolution.
  • The label is not a filter, and the reason for that is now smaller than it was. It records what the tagger read, not what the lore holds, and it labelled NPCs as places often enough that filtering on it would lose them — 17.0 % of the spans landing exactly on an indexed name carried a contradicting family, 5 033 of them a place read as a person. Nerthus.Lang's gazetteer takes that to 6 rows on the same slice, because an indexed name is now labelled by the index rather than guessed. The rule stands anyway until a tree annotated under the gazetteer has been measured through this stage, and it stands permanently for surfaces the index does not carry, which is where the label is still a guess.

A repository with no annotation tree has no Stage 2d and resolves exactly as it did before the layer existed. That degradation is the contract for the whole annotation tree, and it is asserted in the suite rather than assumed.

Stage 3 — Levenshtein fuzzy (BKTree)

The last resort, for typos (ErastrEraster). The index holds a Nerthus.BKTree metric tree over all tokens; Search(query, threshold) prunes subtrees that cannot contain a within-threshold match, giving effective O(log N) lookup over the few-thousand-token index. Distance is two-row, case-insensitive Levenshtein. The threshold is dynamic and length-based, overridable with -MaxDistance:

threshold = MaxDistance >= 0 ? MaxDistance
          : query.Length < 5 ? 1
          : floor(query.Length / 3)

Matches are sorted by distance, then name; each is funneled through the owner filter and the first survivor is the answer. -TopN caps how many alternatives the daemon collects internally, but fuzzy near-misses are not part of the wire response — the response's candidate list carries homonym owners of the matched token.

Structured-value routing always disables fuzzy

This is the rule's normative home: PU character names, @Transfer endpoints, distribution participants, Discord delivery targets, and game-log segment/speaker resolution all resolve with -NoFuzzy — a wrong guess would silently misroute value. Declension and alternation still apply; only the edit-distance stage is off, so an unmatched name returns $null instead of a near-miss. There is no caching of misses.

A query that survives no stage is a hard miss; the PU monthly batch treats any unresolved character name as fatal and aborts with no partial writes (PU model).

The owner filter — -Type, -ActiveOn, -Within

Every stage's candidate token passes through one owner-selection filter:

  1. -Type keeps only owners of the requested type; a Lokacja filter also admits Mapa owners (map-as-location). When both a true Lokacja and a Mapa survive, the exact requested type is preferred.
  2. -ActiveOn <date> (default: now) excludes owners whose @status at that date is Usunięty — owners with any other status, or no @status at all, still resolve. A currently soft-deleted entity therefore never resolves by default, but a historical -ActiveOn predating the deletion still reaches it (temporal value scopes: tag schema).
  3. If multiple owners survive, the resolver picks one — it never returns $null for ambiguity:
  4. -Within <city> wins first. The filter picks the owner whose active @outerior or @lokacja (the computed chain root and the parent location) at -ActiveOn normalizes to the given city. Failing that, it accepts an owner whose @forma_sesyjna (a literal session route form) references that city in the exterior segment before the leaf or the first path segment. This catches a guild canonically seated in Port Tuzmer but written Tuzmer/… in sessions. Name normalization is the deterministic folding owned by Locations.
  5. Temporal preference next: owners carrying @lokacja/@outerior tags with no value active at -ActiveOn (a guild not yet founded) are dropped from the pool. Owners with no containment tags at all are always kept, so Postać/Gracz collisions are never disturbed.
  6. Then the entry's Primary, then the first survivor.
  7. Whatever was picked, the result reports Ambiguous = true whenever more than one owner survived filtering, plus OwnerCandidates — the surviving owner names — so a caller can see that a -Within or -Type hint would make the pick deliberate.

Zero survivors make the stage miss (the pipeline continues); zero survivors at every stage yield $null.

The cast preference — -Cast

A form written inside a session is written in a room, and the room is evidence the stages above cannot see. Riv is a word of the heading Riv Devisson, so the index answers with him for all 1 565 of the archive's spans of it — and 1 407 of those sit in sessions whose cast holds Riveth and not Riv Devisson. Nobody wrote a global @alias: Riv, and nobody should: it is his name too.

-Cast <block names> is how a caller that knows the session says so. It runs after the pipeline, it never changes which stage answered, and it can only ever move an answer the index already gave. A form the stages answer nothing for stays unanswered — «Midaven» is a word of Riveth's alias, which the index does not split, and in the archive's 2024-06-30 it names Daelin on eleven lines and Riveth on two, so a tier that answered there would be wrong more often than right.

Three conditions, then two ways a cast member can carry the form:

  1. The index answered.
  2. The block it answered with is not in the cast. When it is, the answer stands — this is Riv in the 2024-11-15 session whose cast holds Riv Devisson.
  3. Either the token that answered has a cast member among its own ownersAurelia is a heading and the first word of Aurelia Lesup, and only the priority rule chose between them — or a cast member's own name or alias word begins with the queried form, and then the hit must be a fragment: priority 2, or a stage that already approximated. A priority-1 hit on a whole canonical name is the lore naming something outright and no room overrides it.

The result carries Stage = 'cast' and Score = 0.99. Two carriers and neither the index's answer is not a tie-break: both are returned in OwnerCandidates with Ambiguous = true, the shape a shared epithet already uses.

Measured over the 1 464 annotated transcripts before the tier was written (126-ner-next/scripts/84-cast-tier-population.ps1): 79 (form, block) pairs move, 7 203 spans. The largest classes are a player's own name reaching their character (Brimm → Brimm Schadenfreude, 3 065 spans; Fredrick → Fredrick Flumiene), Riv → Riveth (1 389), and a family name the room decides (Dragonius, Schadenfreude, Vruzael).

Who passes a cast: the Kto to? read (Get-NerthusSessionBindingsService), the speech index's per-transcript speaker resolution, and the mention read behind GET /entities/{name}/mentions, which drops a hit whose token the room hands to somebody else. Who does not, and why: the corpus-wide hygiene sweeps have no session by construction, and the archive parse writes a speaker map into the sidecar, where a cast captured at write time would outlive the session record it came from.

-Within and -ActiveOn compose. With Gildia Teologów in Ithan since 2023-01 and a new chapter in Thuzal since 2025-03, -Within Ithan -ActiveOn 2024-01-15 reaches the Ithan chapter and -Within Thuzal -ActiveOn 2025-06-01 the Thuzal one. Without -Within, in 2024 the temporal preference alone picks Ithan, while in 2026 both exist and the pick is flagged Ambiguous.

Cmdlet surface

  • Resolve-NerthusNameService — the daemon-side resolver. Takes -Cast (above) in addition to the filters; a caller with no session passes none and gets exactly the answer it got before the tier existed.
  • Resolve-NerthusName — the resolver. One name resolves singly; several names in -Name switch to batch resolution. Batch supports only -Type, -ActiveOn, and -NoFuzzy; -Within, -TopN, and -MaxDistance apply to single-name resolution. -Type is a closed set — NPC, Grupa, Lokacja, Mapa, Gracz, Postać, Przedmiot — so an off-list value is a bind error, not a silent miss. -MaxDistance defaults to -1 (fall back to the length-based Stage 3 threshold; any value ≥0 overrides it), and -TopN to 1.

Routes, request body fields, response envelopes, capabilities, and the diagnostic GET /v1/api/name-index/lookup/{token} and POST /v1/api/name-index/rebuild (no cmdlet wrappers) are specified in the API reference.

Examples (real Nerthus data)

# Consonant alternation: -dzie reversed to -da; the candidate misses, fuzzy completes
Resolve-NerthusName 'Anwardzie'
#   stage: fuzzy         →  Anward (Gracz), confidence 0.667

# Plain declension suffix strip: -owi
Resolve-NerthusName 'Ithanowi' -Type Lokacja
#   stage: declension    →  Ithan (Lokacja), confidence 0.95

# An -o lemma: the stems don't align, so fuzzy catches it at distance 2
Resolve-NerthusName 'Lossem'
#   stage: fuzzy         →  Losso Minewit (Postać), confidence 0.667

# Homonym + era: three filters composing
Resolve-NerthusName 'Gildia Teologów' -Type Grupa -Within 'Ithan' -ActiveOn 2024-01-15
#   stage: exact         →  Gildia Teologów (Ithan)
#   ambiguous: true; candidates: Gildia Teologów (Ithan), Gildia Teologów (Thuzal)

# Strict resolution for value routing — never guess
Resolve-NerthusName 'Ithn' -NoFuzzy
#   → $null   (exact + declension only; no fuzzy false positive)

# Batch resolution during session distribution
Resolve-NerthusName -Name 'Erastera','Tussalowi','Opat Perrin' -ActiveOn 2026-07-01
#   → one { token, resolved, typ, stage } item per query; stage 'miss' on failure
# Diagnose an ambiguous token without touching daemon internals
curl -s 127.0.0.1:$PORT/v1/api/name-index/lookup/Eraster \
  -H "Authorization: Bearer $TOK"
# → { "source":"Eraster", "ambiguous":true,
#     "owners":[ {"name":"Eraster","type":"Gracz","priority":1}, {"name":"Eraster","type":"Postać","priority":1} ] }

Quantities: how many, of what

A session read answers who. A line like RNdb9gG3:101 — «Straż floty w liczbie pięćdziesięciu. Trzech gońców. Kwatermistrza. Dwa tuziny obsługi» — states four whats, and until the quantity recogniser landed the read answered one row for the whole session and nothing about the guard, the fleet or the fifty.

Two implementations of one grammar, on purpose. nerthus_lifecycle.quantity in Nerthus.Lang is the instrument: it runs beside the parse, over Morfeusz's readings, and its answer travels in the sidecar. Get-NerthusQuantity (daemon/services/Quantity.ps1) is the fallback, for a line whose log has no sidecar, and the cross-check that says the two agree. Where the sidecar carries a quantity the daemon reads it and does not compute its own. Both ship the same data/quantities.json; the numbers in it are the operator's (ruling H126.4, 2026-09-06), and a disagreement between the two readers on an archived log is a listed defect rather than a tie-break.

What the table covers. Digits; inflected cardinals including compounds («stu pięćdziesięciu» is 150, not 100); collective numerals («pięcioro»); collective nouns («trójka»); idioms («tuzin» 12, «kopa» 60, «mendel» 15, «Dwa tuziny» 24, «pół setki» 50); vague quantifiers with published bounds («kilka» 3–9, «kilkanaście» 11–19, «kilkadziesiąt» 20–99, «kilkaset» 200–999); unbounded words («garść», «tłum») with a low bound and no ceiling; approximators («około», «ze», «ponad», «do»); ranges («dwudziestu, trzydziestu», «pięciu albo sześciu»); and distributives («po dwie»).

The head rule is the precision guard. A count attaches only to an ELIGIBLE head — a lexicon role, creature or group, or a supplement head the lexicon lacks (ludzie, osoba, obsługa, flota, karawana, konwój) — and only inside its own clause. Three consequences a reader should be able to predict:

  • A numeral with no eligible head in its clause counts nobody. «Pięćdziesięciu na granicę, osiemdziesiąt do miasta, siedemdziesiąt do Bramy Północy» yields nothing, and the recogniser must not reach back to the line before for a head.
  • An excluded head STOPS the scan rather than being skipped. «dodatkowe dziesięć wozów na całe wyposażenie floty» is ten wagons; a reader that treats wozów as noise walks on and reports a fleet of ten.
  • Money, time, distance and items are excluded by name. «piętnastu talarów» and «dwa tuziny strzał» are silent, which is why talar and srebrnik leave roles at the next format bump.

Scope. A head may carry a scope: a genitive complement right after it («straż floty», «strażnik zakonu»), a classifying adjective on either side («elfia straż», «straż pożarna», «straż miejska» — Polish puts one after the noun as readily as before it), or the clause's own subject when that subject names a place, an organisation or a unit («miasto powinno mieć ze stu pięćdziesięciu strażników»). A subject that is a PERSON is never read as a scope: a unit whose scope is a cast member is that member's, never that member, and binding fifty guards to whoever spoke last is the failure the whole unit path exists to avoid.

Modality and runs. A count is not a fact until something says how it was asserted. An unsigned line is narrator prose and the fiction's ground truth; a signed line is a character asserting something inside the fiction, so asserted_by is a field on every reading rather than a footnote. A line with no cue of its own takes the nearest cue in its run — H126.6's three-minute window — which is how «Potrzebujemy» on RNdb9gG3:100 makes line 101 a plan rather than a report.

Why it works this way

  • Adjectival multi-word inflection. Both words of Stary Werbin decline independently, so each word ≥3 chars is indexed at priority 2 and resolved per-token; whole-phrase declension is not modeled.
  • Alternation before fuzzy. Stage 2b runs ahead of Stage 3 — a validated grammar reversal beats an edit-distance guess.
  • Masculine -o lemmas. Losso-class inflections (Lossem) resolve at Stage 3 rather than Stage 2 (mechanics under the pipeline above).
  • ASCII legacy reads. Diacritic-stripped forms are read on import only; the live index holds diacritical canon (adoption).

See also

  • Architecture — the daemon-as-data-owner contract, the fingerprint self-heal, the Nerthus.* C# substrate
  • API reference — routes, request/response envelopes, capabilities
  • Entities model and the tag schema — token sources (@alias, @slug, @nazwa_nerthus, @base_name, @generyczne_nazwy), @status, temporal value scopes
  • Sessions model — mention extraction and distribution that consume the resolver
  • Players model — the Gracz/Postać split behind the primary-owner rule
  • PU model — fail-early on any unresolved character name
  • Locations model — homonym splits, the deterministic name normalization behind -Within
  • Entity model — the @forma_sesyjna claim ledger and its (auto) markers, beyond the token sources above