{"_id":"@dcsv-io/d2-geo-abstractions","_rev":"2-4db56bafec12885a365338ad0aba5664","name":"@dcsv-io/d2-geo-abstractions","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.1":{"name":"@dcsv-io/d2-geo-abstractions","version":"0.1.1","_id":"@dcsv-io/d2-geo-abstractions@0.1.1","maintainers":[{"name":"dcsv-tristan","email":"tristan@dcsv.io"}],"dist":{"shasum":"7a0885a7181fc4b485f6dce205d3418ce2929d96","tarball":"https://registry.npmjs.org/@dcsv-io/d2-geo-abstractions/-/d2-geo-abstractions-0.1.1.tgz","fileCount":95,"integrity":"sha512-6GYV/Q5Ep6tDu8Kyn3j4zAUC2mG4bvj9Fwnn0SBOl44RXGVl/+6uXzIzSwIqrXxJzdrUWCqlsPw0rX4c7tuhiQ==","signatures":[{"sig":"MEQCIAHA8DvNAWD5dNqXwgrNcG/mjJKmQ0bt1+UQ5wCzjF2fAiA1RVavZY95h2H4jYfO5Ejc2lPa8SQ9clWzrcqQEbayVg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":405958},"main":"./dist/index.js","type":"module","_from":"file:bundle/npm/dcsv-io-d2-geo-abstractions-0.1.1.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc -b","test:coverage":"vitest run --coverage","type-check:test":"tsc -p tsconfig.test.json"},"_npmUser":{"name":"dcsv-tristan","email":"tristan@dcsv.io"},"_resolved":"/home/runner/work/D2-Public/D2-Public/bundle/npm/dcsv-io-d2-geo-abstractions-0.1.1.tgz","_integrity":"sha512-6GYV/Q5Ep6tDu8Kyn3j4zAUC2mG4bvj9Fwnn0SBOl44RXGVl/+6uXzIzSwIqrXxJzdrUWCqlsPw0rX4c7tuhiQ==","_npmVersion":"11.16.0","description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","directories":{},"_nodeVersion":"24.18.0","dependencies":{"@dcsv-io/d2-result":"0.1.1","@dcsv-io/d2-utilities":"0.1.1"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.0.18","typescript":"5.9.3","@vitest/coverage-v8":"4.0.18"},"peerDependencies":{"zod":"4.3.6"},"_npmOperationalInternal":{"tmp":"tmp/d2-geo-abstractions_0.1.1_1784262822082_0.7937423242783059","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@dcsv-io/d2-geo-abstractions","version":"0.1.2","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"peerDependencies":{"zod":"4.3.6"},"dependencies":{"@dcsv-io/d2-result":"0.1.2","@dcsv-io/d2-utilities":"0.1.2"},"devDependencies":{"@vitest/coverage-v8":"4.0.18","typescript":"5.9.3","vitest":"4.0.18"},"scripts":{"build":"tsc -b","test":"vitest run","test:coverage":"vitest run --coverage","type-check:test":"tsc -p tsconfig.test.json"},"_id":"@dcsv-io/d2-geo-abstractions@0.1.2","description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","_integrity":"sha512-Y4ZEePWqjzqUTeJ0Z8WZOXwdyVXYfvy/gOQBfPUajU84+funQmYlzcc1hVmm9gJEcqdZVtzIrpuKg4YHmSDTUg==","_resolved":"/home/runner/work/D2-Public/D2-Public/bundle/npm/dcsv-io-d2-geo-abstractions-0.1.2.tgz","_from":"file:bundle/npm/dcsv-io-d2-geo-abstractions-0.1.2.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-Y4ZEePWqjzqUTeJ0Z8WZOXwdyVXYfvy/gOQBfPUajU84+funQmYlzcc1hVmm9gJEcqdZVtzIrpuKg4YHmSDTUg==","shasum":"4add3d1f9c939eb1378b80c90ab604e3d51e2ffd","tarball":"https://registry.npmjs.org/@dcsv-io/d2-geo-abstractions/-/d2-geo-abstractions-0.1.2.tgz","fileCount":95,"unpackedSize":405864,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDme+HcGpVinw/cW7l9N3Tj5579gHJ+072AfG+o6XwpUAIhAJ3sv4Zl5LfQHUpd8x3K7DwMrCQ5Q8/QxtQqt6vs7eEH"}]},"_npmUser":{"name":"dcsv-tristan","email":"tristan@dcsv.io"},"directories":{},"maintainers":[{"name":"dcsv-tristan","email":"tristan@dcsv.io"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/d2-geo-abstractions_0.1.2_1784286899927_0.47303708818536094"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T04:33:41.976Z","modified":"2026-07-17T11:15:00.225Z","0.1.1":"2026-07-17T04:33:42.223Z","0.1.2":"2026-07-17T11:15:00.083Z"},"description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","maintainers":[{"name":"dcsv-tristan","email":"tristan@dcsv.io"}],"readme":"<!--\nCopyright (c) DCSV. Licensed under the Apache License, Version 2.0.\n-->\n\n# @dcsv-io/d2-geo-abstractions\n\n> **Audience**: backend Node/TypeScript service and BFF engineers who need\n> a data-free reference-data type surface — interfaces, meta-records, and\n> name-resolution primitives — without dragging the full geo catalog\n> (`@dcsv-io/d2-geo-default`).\n\nCodegen-emitted reference-data type surface + hand-written meta-record +\nname-resolution primitives. Mirrors `DcsvIo.D2.Geo.Abstractions` (.NET).\n\n## Install\n\n```bash\npnpm add @dcsv-io/d2-geo-abstractions\n```\n\n## Overview\n\nThe geo reference-data layer ships in two TS packages:\n\n- **`@dcsv-io/d2-geo-abstractions`** — this package. Type shapes (record interfaces,\n branded typed-code wrappers, validation schemas) + `DeprecationInfo` +\n name-resolution helpers. Near-zero runtime payload at import — pure types\n plus two small string-algorithm functions.\n- **`@dcsv-io/d2-geo-default`** — the catalog data itself (~200 KB of country /\n subdivision / currency / language / locale / timezone / geopolitical-entity\n records). Depends on this package.\n\nDomain code that takes a `Country` parameter imports from\n`@dcsv-io/d2-geo-abstractions`; only composition-root / catalog-bootstrap code\nimports `@dcsv-io/d2-geo-default`. This keeps the ~200 KB catalog out of bundles\nthat only need the type shapes.\n\n## Record shape architecture\n\n### Single shape per entity\n\nEvery reference-data catalog ships ONE record interface. Each record carries\nscalars + universal dual-representation for every relationship.\n\n| Catalog                         | Record interface            | Plural data accessor (in `@dcsv-io/d2-geo-default`)    | Lookup table                                                 |\n| ------------------------------- | --------------------------- | ---------------------------------------------- | ------------------------------------------------------------ |\n| Country                         | `Country`                   | `Countries.US`                                 | `CountryLookup.byCode[CountryCode.US]`                       |\n| Subdivision                     | `Subdivision`               | (no plural; use `SubdivisionLookup`)           | `SubdivisionLookup.byCode[\"US-NY\"]`                          |\n| Currency                        | `Currency`                  | `Currencies.USD`                               | `CurrencyLookup.byCode[CurrencyCode.USD]`                    |\n| Language                        | `Language`                  | `Languages.en`                                 | `LanguageLookup.byCode[LanguageCode.en]`                     |\n| Locale                          | `Locale`                    | (no plural; use `LocaleLookup`)                | `LocaleLookup.byTag[\"en-US\"]`                                |\n| Timezone                        | `Timezone`                  | (no plural; use `TimezoneLookup`)              | `TimezoneLookup.byCode[\"America/New_York\"]`                  |\n| GeopoliticalEntity              | `GeopoliticalEntity`        | `GeopoliticalEntities.EU`                      | `GeopoliticalEntityLookup.byCode[GeopoliticalEntityCode.EU]` |\n| CountryCurrencyAcceptance (M:M) | `CountryCurrencyAcceptance` | (denormalized payload on `country.currencies`) | (no lookup — it's a join shape)                              |\n\n### Universal dual representation\n\nEvery relationship on every record carries BOTH a typed code field AND a nav\nrecord field:\n\n| Cardinality   | Code rep                                                                                     | Nav rep                                                    |\n| ------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |\n| **Single FK** | `{relationship?}{StandardName}?: TCode` (`primaryLanguageIso6391Code?: LanguageCode`)        | `{relationship?}?: TRecord` (`primaryLanguage?: Language`) |\n| **Set FK**    | `{relationship?}{StandardName}s: ReadonlySet<TCode>` (`Set<TCode>`-backed for O(1) `.has()`) | `{relationship?}s: readonly TRecord[]` (ordered iteration) |\n\nThe code rep enables O(1) membership checks\n(`country.geopoliticalEntityShortCodes.has(GeopoliticalEntityCode.EU)`); the\nnav rep enables ordered iteration and property access\n(`for (const member of eu.memberCountries) ...`). Both forms are always\npresent; neither replaces the other.\n\nNullable single-primary navs use `?:` (per the workspace `undefined`-over-\n`null` convention) — `undefined` for uninhabited territories (AQ / BV / HM)\non `country.primaryLanguage` / `primaryCurrency` / `primaryLocale`.\n\n### PK + FK naming convention\n\n- **Name** describes WHAT the value IS: `iso31661Alpha2Code`,\n `ietfBcp47Tag`, `ianaName`. Never bare `code` on its own.\n- **Type** is the typed code wrapper (`CountryCode`, `SubdivisionCode`,\n `LocaleCode`, …).\n- **Relationship prefix** on FKs disambiguates direction / cardinality:\n `primary` (primary among possibly many), `sovereign` / `territory`\n (hierarchy direction), `member` (group membership), `spokenIn` /\n `acceptedIn` (reverse \"consumed by\"), `coApplicable` (parallel beyond a\n primary).\n\n### Code-suffix naming on closed-set enums\n\nClosed-set catalog-identifier enums carry the `Code` suffix to disambiguate\nfrom the record shape:\n\n- **`Code`-suffixed enums**: `CountryCode`, `CurrencyCode`, `LanguageCode`,\n `GeopoliticalEntityCode`.\n- **Type-discriminator enums (no `Code` suffix)**: `GeopoliticalEntityType`,\n `WritingDirection`, `DateFormatPattern`, `CurrencyAcceptanceLevel`,\n `MeasurementSystem`, `DayOfWeek`.\n- **Open-set branded wrappers (names already disambiguate)**:\n `SubdivisionCode`, `LocaleCode`, `TimezoneCode`.\n\n### Cycle resolution — multi-pass cast pattern\n\nCyclic record graphs (`country.primaryLanguage → language.spokenInCountries\n→ country`) are resolved at codegen time via a multi-pass declare-then-mutate\npattern. Since TS `readonly` is compile-time only (no runtime enforcement),\nthe data emitter declares records with code-rep fields populated and nav-rep\nfields at defaults (`undefined` / `[]`) in the first pass, then mutates nav\nrefs via a one-time type cast in the wire-nav step:\n\n```ts\n// First pass — declare with code-rep populated and nav-rep at defaults.\nconst us: Country = {\n  iso31661Alpha2Code: CountryCode.US,\n  displayName: \"United States\",\n  primaryLanguageIso6391Code: LanguageCode.en, // code rep\n  territoryIso31661Alpha2Codes: new Set<CountryCode>([\n    CountryCode.PR /* ... */,\n  ]),\n  subdivisionIso31662Codes: new Set<SubdivisionCode>(),\n  subdivisions: [],\n  primaryLanguage: undefined, // nav rep defaults\n  // ... rest of fields ...\n};\n\n// Wire-nav step — populate nav refs via cast (one-time mutation).\n(us as { -readonly [K in keyof Country]: Country[K] }).primaryLanguage =\n  LanguageLookup.byCode[LanguageCode.en];\n```\n\nThe cast is confined to codegen-emitted module-init code under\n`@dcsv-io/d2-geo-default`. Hand-written consumer code MUST treat the record fields\nas `readonly` (compile-time enforced). Wiring nav refs outside of\ncodegen-emitted module init is a hand-written-code-touching-codegen-territory\nbug.\n\n## Public surface\n\nThe hand-written surface in this package is intentionally tiny — the bulk\nof the type catalog materializes under `src/generated/` from the same JSON\nspecs that drive the .NET source-generator. Hand-written files:\n\n| Export                         | Source file                                   | Purpose                                                                                                                       |\n| ------------------------------ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| `DeprecationInfo` (interface)  | `src/deprecation-info.ts`                     | Meta-record carried on every catalog record as an optional `deprecation` field. Mirrors .NET `DeprecationInfo` sealed record. |\n| `IGeoReference` (interface)    | `src/interfaces/i-geo-reference.ts`           | Strongly-typed lookup contract — `getCountry(code)`, `getSubdivision(code)`, etc. Returns the single record shape.            |\n| `IGeoNameResolver` (interface) | `src/name-resolution/i-geo-name-resolver.ts`  | Free-form text → entity resolver contract. Returns full records (not codes) so caller sees resolved record immediately.       |\n| `normalize(input)`             | `src/name-resolution/name-normalizer.ts`      | NFD-fold + diacritic-strip + locale-invariant lowercase + whitespace normalize + `&`↔`and` substitution.                      |\n| `compare(a, b, maxDistance)`   | `src/name-resolution/levenshtein-comparer.ts` | Classic Wagner-Fischer Levenshtein with early-termination at `maxDistance + 1`.                                               |\n| `isWithin(a, b, maxDistance)`  | `src/name-resolution/levenshtein-comparer.ts` | Convenience predicate over `compare`.                                                                                         |\n\nThe codegen-emitted spec-derived type catalog (record shapes, branded\ntyped-code wrappers, Zod schemas, `GEO_CATALOG_VERSION`) lives under\n`src/generated/`.\n\n## Codegen pattern\n\nThis package follows the standard `the TypeScript codegen pipeline` pattern: hand-written\nfiles live directly under `src/`, codegen-emitted files materialize under\n`src/generated/` and are tracked in git so PR reviewers see codegen diffs\nwithout a local build. The emitter is `the TypeScript codegen pipeline/src/geo-emitter/`\nand runs as part of the workspace codegen orchestrator (`pnpm codegen`).\n\n## Parity with .NET\n\nMirrors `DcsvIo.D2.Geo.Abstractions`:\n\n- `DeprecationInfo` ↔ `DcsvIo.D2.Geo.Abstractions.DeprecationInfo` —\n same four fields, same JSON wire shape. `DateOnly DeprecatedAt`\n serializes to ISO-8601 calendar-date string; the TS-side mirror uses\n `string` carrying the same `YYYY-MM-DD` text.\n- Record shapes — every `Country` / `Subdivision` / `Currency` / `Language`\n / `Locale` / `Timezone` / `GeopoliticalEntity` field is byte-for-byte\n parity with the .NET counterpart (modulo TS casing — `iso31661Alpha2Code`\n on TS ↔ `Iso31661Alpha2Code` on .NET).\n- `normalize` ↔ `NameNormalizer.Normalize` — same six-step pipeline.\n Cross-language parity is pinned by a byte-equivalent fixture.\n- `compare` / `isWithin` ↔ `LevenshteinComparer.Compare` /\n `LevenshteinComparer.IsWithin` — same Wagner-Fischer DP, same\n early-termination sentinel (`maxDistance + 1`).\n\nOptional fields use `undefined` (not `null`) per workspace TS convention.\n`null` arriving from the .NET wire normalizes to `undefined` at the Zod\ndeserialization boundary.\n\n## Dependencies\n\n- `zod` — runtime dep for the codegen-emitted Zod schemas\n (`subdivision-code.g.ts`, `fixed-enums.g.ts`, etc.).\n\n## Telemetry\n\nNo telemetry surface — foundation lib emits no spans or metrics. Consumers\ninstrument the resolver call sites in their own OTel setup.\n\n## Configuration\n\nNo configuration — zero-config; the catalog version is baked in at codegen\ntime via `GEO_CATALOG_VERSION`.\n\n## Known build-time warnings\n\nThe TS geo emitter (codegen geo-emitter) emits a small\nnumber of expected `D2GEO010` catalog-uniqueness warnings (legitimate\nparent-child name collisions like Burkina Faso Centre / Kadiogo,\nreal-world ambiguity like Malta's two Rabats). See\n`contracts/geo/KNOWN_WARNINGS.md`\nfor the full enumerated list + escalation triggers. New warnings NOT\ndocumented there should be investigated before suppressing.\n","readmeFilename":"README.md"}