OPENNLP-1879: Gazetteer and geocoder API with a bundled Natural Earth implementation#1154
Draft
krickert wants to merge 6 commits into
Draft
OPENNLP-1879: Gazetteer and geocoder API with a bundled Natural Earth implementation#1154krickert wants to merge 6 commits into
krickert wants to merge 6 commits into
Conversation
f0adc29 to
6e5a2d8
Compare
Define the location-intelligence seam in opennlp-api: GeoPoint (WGS84, range-validated), GazetteerEntry (immutable, defensive copies, provenance-tagged attribute map), AttributeValue, Gazetteer (name, id, and ISO 3166-1 alpha-2 region lookup), GeoResolution (mention span in original document coordinates, confidence in [0, 1]), and Geocoder. The shape is deliberately generic: neutral field names, source-scoped identifiers, and dataset-specific extras carried as provenance-tagged attribute values, so bundled, downloaded, and user-supplied datasets all sit behind the same interface. Attribute keys for stable external identifiers (fips, geoid, zcta, wikidata, geonames, whosonfirst) follow a documented convention so downstream enrichment data can join through an entry without any dataset being bundled. The country code is the join key shared with the emoji annotation layer. No new dependencies in opennlp-api.
New opennlp-extensions/opennlp-geo module (spellcheck precedent: the core jars stay lean) holding the reference Gazetteer implementation. BundledGazetteer reads a semicolon separated populated-places table (source;recordId;name;altNames;lat;lon;iso2;containment;population; featureClass;attributes), cursor-parsed line by line with fail-loud IllegalArgumentException naming the resource and line for malformed rows, loaded lazily once and immutable after load. Both indexed names and queries fold through the same matching chain (UAX apache#29 word segmentation, then NFC, case fold, accent fold per token), so case, accent, and hyphenation variants of a name produce the same match key. Lookup results rank by population descending, then a CITY over ADMIN over POI feature-class prior, then source and record id, giving a deterministic total order. byRegion serves the ISO 3166-1 alpha-2 join key with the most populous entry for the region. The bundled data table itself follows in the next commit; tests here run against crafted in-memory rows.
Adds the project-authored gazetteer table derived from the Natural Earth 1:10m Populated Places dataset (public domain): 7342 rows, one per populated place, with the NE id as the stable record id, the transliterated ASCII name (NAMEASCII), alternate names, WGS84 point coordinates, ISO 3166-1 alpha-2 country code, the admin-1 name as the v1 containment element, POP_MAX, featureClass CITY, and the external identifiers Natural Earth publishes (wikidata, geonames, whosonfirst) as provenance-tagged attributes. The full derivation record (source, mirror commit, extraction date, field mapping, public-domain dedication) is in the file header. The table is deliberately pure ASCII in v1: non-ASCII primary names are carried as the ASCII transliteration Natural Earth provides, with the Unicode forms omitted; the folded matching chain keeps accented queries working against the ASCII rows. The audit test enforces the header's claims row by row: row-count floor, pure-ASCII bytes, no duplicate ids, country-code shape (the emoji-layer join key), lat/lon range, the attribute-key whitelist, and deterministic lookups. LICENSE gains the Natural Earth public-domain section; per the ASF guidance for public-domain data there is no NOTICE entry.
Resolves each location mention independently: candidates come from the gazetteer lookup and are re-ranked here under the module's shared deterministic order (population descending, CITY over ADMIN over POI on population ties, then source and record id), so the result never depends on the gazetteer's return order. Unresolved mentions are omitted; results stay aligned to the input spans in input order. Confidence is an explicitly documented heuristic prior, not a probability: 0.9 for a single candidate, otherwise 0.5 + 0.4 * (p1 - p2) / (p1 + p2), monotonic in the relative population separation between winner and runner-up, 0.5 on a dead tie, and never 1.0. Document-context-aware scoring (co-occurring mentions constraining each other) is the named follow-up as a second Geocoder implementation; resolve() already receives the whole document for that reason. The candidate order moves into a shared package-private CandidateRanking used by both BundledGazetteer and the geocoder.
Contributor
|
Hi @krickert - as mentioned on Slack I currently dont have the time for a manual review but I just let Fable do a comprehensive review on this PR. Here is the result: Correctness / API design
Minor
Human review will follow. |
… gazetteer seam The bundled Natural Earth table is now produced by a committed generation script (opennlp-geo/dev/derive-populated-places.py) that detects and handles the encoding damage the upstream distribution itself carries. The canonical name derives from the upstream display name folded to ASCII, with the upstream transliteration as a fallback and, where it differs, as an alternate name; values that announce damage with '?' are omitted and counted, values verified against accent-bearing upstream siblings or documented place names are repaired through audited tables in the script, and one-character corruptions of a name at an accented position are dropped. The script hard-fails on anomalies it does not recognize, so a data refresh forces a reviewed decision. The regenerated table changes 181 of 7342 rows. The data file now carries the derivation record and a LICENSE pointer instead of an ASF source header, is listed in rat-excludes, and the Natural Earth public-domain section is added to the binary distribution LICENSE alongside the existing root LICENSE section. On the seam, Gazetteer.lookup, byId, byRegion and Geocoder.resolve declare IOException so database-backed and remote implementations have a portable failure channel, matching the EntityLinker precedent, and both interfaces now cross-reference opennlp.tools.entitylinker with guidance on which contract new consumers should target. The feature-class and attribute-key vocabularies are published as constants on GazetteerEntry, the UNSPECIFIED provenance marker as a constant on AttributeValue, and CandidateRanking matches on the constants so any gazetteer using them gets the feature-class prior. The population zero sentinel is documented as a deliberate choice inherited from upstream conventions, and javadoc references to an unshipped annotation layer are rephrased to the underlying ISO 3166-1 join-key fact. BundledGazetteer gains a public fromEntries factory so callers can index their own rows without the singleton, and a name that folds to an empty match key now fails loud at load time instead of loading an unreachable record. PopulationPriorGeocoder computes confidence in double with clamping so adversarial populations near Long.MAX_VALUE cannot break the GeoResolution contract, and skips the per-mention copy and sort for single-candidate lookups. The audit test enforces the header row count exactly, rejects mojibake artifact patterns and damage markers across every name, alternate name, and containment value, and asserts every name yields a non-empty fold key; parser fail-loud coverage now includes duplicate attribute keys, empty pipe-list elements, and out-of-range longitude, with use-case tests for the custom-entries factory and the third-party feature-class prior.
Contributor
Author
|
Thanks, the data finding especially. Addressed in 224cfe6:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the location-intelligence seam and its first implementation.
Contracts in opennlp-api (
opennlp.tools.geo):GeoPoint,GazetteerEntry,AttributeValue,Gazetteer,GeoResolution,Geocoder. Generic in shape: the dataset is an implementation, never the contract. Attributes are provenance-tagged, with a documented external-id key convention (fips,geoid,zcta,wikidata,geonames,whosonfirst) so downstream enrichment joins on stable keys, and ISO 3166-1 alpha-2 is the region join key shared with the emoji annotation layer. No new dependencies.Implementation in a new
opennlp-extensions/opennlp-geomodule:BundledGazetteerover a project-authored 7,342-row public-domain table derived from Natural Earth Populated Places: fail-loud cursor parser, lazy single load, matching through the shared NFC/case/accent folding chain.PopulationPriorGeocoder: deterministic population-plus-feature-class ranking, documented heuristic confidence, unresolved mentions omitted.LICENSE gains the Natural Earth public-domain section; audit tests enforce the data file's documented claims row by row.
Data derivation: Natural Earth 1:10m Populated Places via the nvkelso GitHub mirror (upstream commit 789c9904, VERSION 5.2.0-pre), NAMEASCII canonical, wikidata/geonames/whosonfirst ids carried as published, no fabricated fips/geoid/zcta.
Verification: api 328/0 (42 new), opennlp-geo 55/0, 19-module reactor verify green.
Follow-ups (deliberately out of this PR): opennlp-distr wiring plus the binary LICENSE section, a context-minimization
Geocoder, the Overture and GeoNames tiers, and the gRPC mirror after one review round.https://issues.apache.org/jira/browse/OPENNLP-1879