From 2e62747ca48cb16cfebf73e62fe4ae0db23f8557 Mon Sep 17 00:00:00 2001 From: SecureCloudGroup Date: Mon, 28 Sep 2026 16:41:25 -0400 Subject: [PATCH 1/2] Library API service, sourcetool ingest, and place nicknames The Library API (ruling R7): service/ is a small FastAPI + SQLite service for https://smartbrain.securecloudgroup.com/library/v1/ that takes votes and suggestions from the app, serves the pack manifest, and exposes a token-guarded queue to the operator. Suggestions run the same validate_record rules as CI plus sourcetool/submission.py (no parameter values, no credentials, public hosts only, known fields, size caps); trust fields are set server-side. No accounts, cookies or identifiers; client addresses live only in the in-memory rate limiter. Deploy files (systemd unit, Caddy snippet, env and manifest examples) and steps in service/README.md; not deployed. sourcetool ingest pulls the queue, re-checks every item, adds votes to records, writes new suggestions to sources/suggested/.jsonl (tier harvested, origin user-suggestion), probes them, and clears the queue. check and build now read sources/suggested/. The schema also refuses private IP literals and user:pass@. Place nicknames: Wikidata short names and nicknames joined to Census places by GNIS id, plus a reviewed list (resolvers/place_nicknames.json); 69 places gain 108 aliases. A two-letter place alias counts only in capitals, and a state code that is also a nickname ("LA", "DC") names the place only when no other place is named. A code that names the ask's place is not a ticker ("NYC weather"). --- .github/workflows/ci.yml | 10 + .gitignore | 2 + README.md | 2 + docs/PLAN.md | 16 +- docs/RESOLVERS.md | 17 +- docs/SCHEMA.md | 6 +- resolvers/place.jsonl | 138 +++++----- resolvers/place_nicknames.json | 38 +++ service/README.md | 109 ++++++++ service/__init__.py | 0 service/app.py | 305 ++++++++++++++++++++++ service/deploy/env.example | 7 + service/deploy/library.caddy | 12 + service/deploy/manifest.example.json | 6 + service/deploy/smartbrain-library.service | 48 ++++ service/requirements-test.txt | 2 + service/requirements.txt | 6 + service/tests/__init__.py | 0 service/tests/test_service.py | 242 +++++++++++++++++ sourcetool/__main__.py | 10 +- sourcetool/build.py | 6 +- sourcetool/common.py | 15 +- sourcetool/ingest.py | 162 ++++++++++++ sourcetool/resolve.py | 16 +- sourcetool/resolvers.py | 75 +++++- sourcetool/schema.py | 39 ++- sourcetool/submission.py | 151 +++++++++++ tests/lookup_asks.json | 2 + tests/resolution_asks.json | 66 +++++ tests/test_ingest.py | 117 +++++++++ 30 files changed, 1524 insertions(+), 101 deletions(-) create mode 100644 resolvers/place_nicknames.json create mode 100644 service/README.md create mode 100644 service/__init__.py create mode 100644 service/app.py create mode 100644 service/deploy/env.example create mode 100644 service/deploy/library.caddy create mode 100644 service/deploy/manifest.example.json create mode 100644 service/deploy/smartbrain-library.service create mode 100644 service/requirements-test.txt create mode 100644 service/requirements.txt create mode 100644 service/tests/__init__.py create mode 100644 service/tests/test_service.py create mode 100644 sourcetool/ingest.py create mode 100644 sourcetool/submission.py create mode 100644 tests/test_ingest.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97a6407..0e17da3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,16 @@ jobs: - name: "source lookup (development + held-out; the first source offered must be right)" run: | python -m sourcetool evallookup lookup_asks lookup_holdout || echo "::warning::lookup below 100%, see output" + service: + name: Library API + ingest tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -r service/requirements-test.txt + - run: python -m pytest service/tests tests/test_ingest.py -q revalidate: name: weekly re-validation (report only) if: github.event_name == 'schedule' diff --git a/.gitignore b/.gitignore index c01de9b..e1c9266 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ build/ __pycache__/ *.pyc .DS_Store +.venv-service/ +service/data/ diff --git a/README.md b/README.md index 01ecb2b..0a61e08 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,11 @@ python3 -m venv .venv && .venv/bin/pip install httpx duckdb | `taxonomy/` | categories, subcategories, question kinds, parameter kinds | | `sources/curated/` | hand-curated records (JSONL, one per line) | | `sources/harvested/` | records harvested from open catalogs (labelled, ranked lower) | +| `sources/suggested/` | users' suggestions, written by `sourcetool ingest` | | `providers/` | provider records | | `resolvers/` | lists that turn words into parameters (status pages, stations…) | | `sourcetool/` | the tool | +| `service/` | the Library API the app submits votes and suggestions to ([README](service/README.md)) | | `docs/` | [PLAN](docs/PLAN.md), [POLICY](docs/POLICY.md), [SCHEMA](docs/SCHEMA.md) | Only the maintainer merges changes to `main`. Changes from SmartBrain users arrive through the Library API, and are then reviewed and opened as PRs by the maintainer's tooling. diff --git a/docs/PLAN.md b/docs/PLAN.md index fcba4df..bbf7f26 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -68,7 +68,7 @@ These are the Library records (`docs/SCHEMA.md`). | `build` | Compile `build/library.duckdb`: sources, categories, taxonomy and a term index for scoring. | | `lookup "words"` | Look up a phrase against the build (the same SQL the app runs). | | `coverage` | Count sources per subcategory and list the empty ones. | -| `ingest` (phase 2) | Pull the API submission queue, validate, aggregate votes, write records, and open a PR for the operator. | +| `ingest` | Pull the API submission queue, check it again, aggregate votes, write suggestions to `sources/suggested/`, probe them, and clear the queue. The operator opens the PR (`--commit` makes the branch and commit). | `sourcetool` writes only to a checkout. Publishing is always a PR that the operator merges. @@ -90,16 +90,18 @@ No and "broken" reports go the same way, as votes against a source. This is a small service on the existing VPS (`smartbrain.securecloudgroup.com`) next to the landing page and broker. It reuses the VPS deploy and TLS. +The service is `service/` in this repo (`service/README.md` has the deploy steps). + | Endpoint | Purpose | |---|---| -| `POST /library/v1/votes` | `{source_id, verdict: yes\|no\|broken}` for known sources | -| `POST /library/v1/suggestions` | A new source record (from the Library form or a Yes on a web source) | -| `GET /library/v1/manifest` | The latest signed pack: seq, sha256, size, url | -| `GET /library/v1/pack/{seq}` | The signed pack (the DuckDB file, or JSONL plus the build step in the app) | +| `POST /library/v1/votes` | `{source_id, verdict: yes\|no\|broken, app_version}` for known sources | +| `POST /library/v1/suggestions` | `{record, via: form\|yes, app_version}`: a new source record (from the Library form or a Yes on a web source) | +| `GET /library/v1/manifest` | The latest pack: tag, url, sha256, bytes (the pack itself stays on the GitHub release, R12) | +| `GET /library/v1/admin/queue`, `POST /library/v1/admin/ack` | The operator's `sourcetool ingest` (bearer token) | Security: - Anonymous: no account, no install id, no cookies. -- Rate-limited per IP, with a small proof-of-work on submissions. +- Rate-limited per IP in memory only (30 votes and 10 suggestions per hour). A proof-of-work is not built yet. - Size caps. - Every payload runs through the same `validate_record` rules as CI: https, public host, no credentials, known categories. - The service **never fetches** a submitted URL; `sourcetool ingest` validates on the operator's side. @@ -134,7 +136,7 @@ Flow: the queue feeds `sourcetool ingest` on the operator's side. It validates, | A | Taxonomy v1, schema, `sourcetool` check, validate, harvest, build and lookup; curated US core; 7 harvesters; DuckDB build | **done** | | B | Repo public and protected; CI (schema check plus validation of changed records); scheduled re-validation | this change | | C | App: signed pack install, DuckDB tables, lookup in the NI finder, Yes recorded locally, the Library page and form | next (SmartBrain_3000 PR) | -| D | The Library API on the VPS, `sourcetool ingest`, vote aggregation, and the Yes vote plus suggestions from the app | after C | +| D | The Library API on the VPS, `sourcetool ingest`, vote aggregation, and the Yes vote plus suggestions from the app | service and ingest built; VPS deploy and the app's calls next | | E | Model-drafted classification (R3) to fix keyword misplacements; more harvesters (state 511 and GTFS registries, NWS stations, NOAA stations as resolvers) | rolling | ## Known gaps (v1) diff --git a/docs/RESOLVERS.md b/docs/RESOLVERS.md index e6696c8..8f6a14e 100644 --- a/docs/RESOLVERS.md +++ b/docs/RESOLVERS.md @@ -10,7 +10,7 @@ A resolver is a table of entries, one per line in `resolvers/.jsonl`, all | Resolver | Entries | Open source | |---|---|---| -| place | 32,333 | Census gazetteer plus population estimates | +| place | 32,333 | Census gazetteer plus population estimates; nicknames from Wikidata and `resolvers/place_nicknames.json` | | zip | 33,791 | Census ZIP code areas (ZCTAs) | | county | 3,222 | Census gazetteer | | us_state | 56 | fixed list | @@ -33,6 +33,17 @@ The single matcher is `sourcetool/resolve.py`. It works in three ways: - **`by_name(many=True)`** returns every distinct thing named, in order: a currency pair, or "compare X and Y". - **`near`** returns the nearest entries within the policy's distance. When a near rival differs in what it measures (`differ_on`, such as a different body of water or river), the result is *ambiguous*, which means **ask the user**. When nothing is in range, it's an honest *none*. +### Place nicknames +People say "NYC", "LA", "Philly", "Vegas", "NOLA", "Chi-town" or "the Big Apple". These are extra aliases on the place: +- **Wikidata**: English short names (P1813) and nicknames (P1449) of items with a GNIS id (P590), joined to the Census place by the gazetteer's GNIS code (ANSICODE). The join keeps only items that are a Census place. A nickname is kept when it is a code typed in capitals ("NYC", "L.A."), a single word of five or more letters ("Philly"), or a phrase of up to three words ("Mile High City"); slogans, anything with a digit, short everyday words ("Jeff", "The Hub") and state names are dropped. A Wikidata nickname never shadows another place's own name ("Frisco" stays Frisco, Texas). +- **Reviewed list** `resolvers/place_nicknames.json`: the common ones Wikidata lacks (it has none for Chicago), plus an `exclude` list for Wikidata nicknames we refuse. + +Two matcher rules keep short nicknames honest: +- A **two-letter** place alias counts only when typed in capitals: "LA weather" is Los Angeles, the word "la" never is. +- A capitalised **state code that is also a place nickname** ("LA", "DC") names that place only when no other place is named. "LA weather" is Los Angeles; "Lafayette LA" is Lafayette, Louisiana, because another place is named and the code is its state. + +In source lookup, a code typed in capitals that names the ask's place ("NYC weather") is the place, not a ticker, unless the ask has a ticker cue ("NYC stock"). + Every result is `resolved`, `ambiguous` (the card asks one question, offering the candidates) or `none`. Nothing is guessed silently. ## Fills: how every source parameter gets its value @@ -74,7 +85,7 @@ Resolver sets: | Set | Size | Result | |---|---|---| -| Development | 60 asks | 100% | +| Development | 71 asks (11 nickname asks added) | 100% | | Held-out | 44 asks | 82% on its first run, then 100% after general fixes | | Sealed | 40 asks, run once | **90%** | @@ -82,7 +93,7 @@ Lookup sets (top-1): | Set | Size | Result | |---|---|---| -| Development | 45 asks | 100% | +| Development | 47 asks | 100% | | Held-out | 40 asks | 85% on its first run, then 95% after general fixes | | Sealed | 30 asks, run once | **100%** | diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md index a5b68a3..d7182ae 100644 --- a/docs/SCHEMA.md +++ b/docs/SCHEMA.md @@ -1,6 +1,6 @@ # Source record schema (v1) -Each source is one JSON object per line in `sources/**/*.jsonl`. `sourcetool/schema.py` holds the rules, and CI, `sourcetool` and the app all run the same validator. +Each source is one JSON object per line in `sources/**/*.jsonl`. Users' suggestions arrive through the Library API and `sourcetool ingest` writes them to `sources/suggested/.jsonl` with tier `harvested` and origin `{by: "user-suggestion"}`. `sourcetool/schema.py` holds the rules, and CI, `sourcetool` and the app all run the same validator. | Field | Meaning | |---|---| @@ -29,8 +29,8 @@ Each source is one JSON object per line in `sources/**/*.jsonl`. `sourcetool/sch Rules that reject a record: - non-https URLs; -- private or `.local` hosts; -- a credential embedded in the URL; +- private or `.local` hosts, and private, loopback or link-local IP addresses; +- a credential embedded in the URL (a key parameter or `user:password@`); - unknown categories, kinds or parameter kinds; - undeclared `{params}`; - duplicate ids. diff --git a/resolvers/place.jsonl b/resolvers/place.jsonl index b865b90..648024e 100644 --- a/resolvers/place.jsonl +++ b/resolvers/place.jsonl @@ -47,7 +47,7 @@ {"aliases": ["berry", "berry al", "berry alabama"], "attrs": {"land_km2": 27.9, "pop": 1182, "type": "town"}, "id": "place:0105932", "key": "0105932", "kind": "place", "lat": 33.666702, "lon": -87.609311, "name": "Berry", "rank": 3.07, "resolver": "place", "state": "AL"} {"aliases": ["bessemer", "bessemer al", "bessemer alabama"], "attrs": {"land_km2": 105.3, "pop": 25079, "type": "city"}, "id": "place:0105980", "key": "0105980", "kind": "place", "lat": 33.370858, "lon": -86.971522, "name": "Bessemer", "rank": 4.4, "resolver": "place", "state": "AL"} {"aliases": ["billingsley", "billingsley al", "billingsley alabama"], "attrs": {"land_km2": 3.0, "pop": 128, "type": "town"}, "id": "place:0106460", "key": "0106460", "kind": "place", "lat": 32.664711, "lon": -86.706713, "name": "Billingsley", "rank": 2.11, "resolver": "place", "state": "AL"} -{"aliases": ["birmingham", "birmingham al", "birmingham alabama"], "attrs": {"land_km2": 380.9, "pop": 196357, "type": "city"}, "id": "place:0107000", "key": "0107000", "kind": "place", "lat": 33.527174, "lon": -86.797036, "name": "Birmingham", "rank": 5.29, "resolver": "place", "state": "AL"} +{"aliases": ["birmingham", "birmingham al", "birmingham alabama", "magic city"], "attrs": {"land_km2": 380.9, "pop": 196357, "type": "city"}, "id": "place:0107000", "key": "0107000", "kind": "place", "lat": 33.527174, "lon": -86.797036, "name": "Birmingham", "rank": 5.29, "resolver": "place", "state": "AL"} {"aliases": ["black", "black al", "black alabama"], "attrs": {"land_km2": 10.5, "pop": 241, "type": "town"}, "id": "place:0107120", "key": "0107120", "kind": "place", "lat": 31.00977, "lon": -85.74442, "name": "Black", "rank": 2.38, "resolver": "place", "state": "AL"} {"aliases": ["blountsville", "blountsville al", "blountsville alabama"], "attrs": {"land_km2": 14.0, "pop": 1820, "type": "town"}, "id": "place:0107456", "key": "0107456", "kind": "place", "lat": 34.075838, "lon": -86.583918, "name": "Blountsville", "rank": 3.26, "resolver": "place", "state": "AL"} {"aliases": ["blue ridge", "blue ridge al", "blue ridge alabama"], "attrs": {"land_km2": 20.1, "pop": 0, "type": "cdp"}, "id": "place:0107588", "key": "0107588", "kind": "place", "lat": 32.4989, "lon": -86.182436, "name": "Blue Ridge", "rank": 0.0, "resolver": "place", "state": "AL"} @@ -687,7 +687,7 @@ {"aliases": ["eureka roadhouse", "eureka roadhouse ak", "eureka roadhouse alaska"], "attrs": {"land_km2": 481.2, "pop": 0, "type": "cdp"}, "id": "place:0223720", "key": "0223720", "kind": "place", "lat": 61.958507, "lon": -147.155104, "name": "Eureka Roadhouse", "rank": 0.0, "resolver": "place", "state": "AK"} {"aliases": ["evansville", "evansville ak", "evansville alaska"], "attrs": {"land_km2": 59.2, "pop": 0, "type": "cdp"}, "id": "place:0223790", "key": "0223790", "kind": "place", "lat": 66.932933, "lon": -151.37006, "name": "Evansville", "rank": 0.0, "resolver": "place", "state": "AK"} {"aliases": ["excursion inlet", "excursion inlet ak", "excursion inlet alaska"], "attrs": {"land_km2": 146.1, "pop": 0, "type": "cdp"}, "id": "place:0223900", "key": "0223900", "kind": "place", "lat": 58.415144, "lon": -135.345462, "name": "Excursion Inlet", "rank": 0.0, "resolver": "place", "state": "AK"} -{"aliases": ["fairbanks", "fairbanks ak", "fairbanks alaska"], "attrs": {"land_km2": 82.4, "pop": 31732, "type": "city"}, "id": "place:0224230", "key": "0224230", "kind": "place", "lat": 64.836531, "lon": -147.651745, "name": "Fairbanks", "rank": 4.5, "resolver": "place", "state": "AK"} +{"aliases": ["fairbanks", "fairbanks ak", "fairbanks alaska", "golden heart city"], "attrs": {"land_km2": 82.4, "pop": 31732, "type": "city"}, "id": "place:0224230", "key": "0224230", "kind": "place", "lat": 64.836531, "lon": -147.651745, "name": "Fairbanks", "rank": 4.5, "resolver": "place", "state": "AK"} {"aliases": ["false pass", "false pass ak", "false pass alaska"], "attrs": {"land_km2": 64.9, "pop": 404, "type": "city"}, "id": "place:0224660", "key": "0224660", "kind": "place", "lat": 54.872441, "lon": -163.334268, "name": "False Pass", "rank": 2.61, "resolver": "place", "state": "AK"} {"aliases": ["farmers loop", "farmers loop ak", "farmers loop alaska"], "attrs": {"land_km2": 56.4, "pop": 0, "type": "cdp"}, "id": "place:0224980", "key": "0224980", "kind": "place", "lat": 64.908228, "lon": -147.698608, "name": "Farmers Loop", "rank": 0.0, "resolver": "place", "state": "AK"} {"aliases": ["farm loop", "farm loop ak", "farm loop alaska"], "attrs": {"land_km2": 23.6, "pop": 0, "type": "cdp"}, "id": "place:0225000", "key": "0225000", "kind": "place", "lat": 61.635115, "lon": -149.155223, "name": "Farm Loop", "rank": 0.0, "resolver": "place", "state": "AK"} @@ -1225,7 +1225,7 @@ {"aliases": ["peeples valley", "peeples valley arizona", "peeples valley az"], "attrs": {"land_km2": 39.2, "pop": 0, "type": "cdp"}, "id": "place:0453910", "key": "0453910", "kind": "place", "lat": 34.277224, "lon": -112.765341, "name": "Peeples Valley", "rank": 0.0, "resolver": "place", "state": "AZ"} {"aliases": ["peoria", "peoria arizona", "peoria az"], "attrs": {"land_km2": 457.7, "pop": 199924, "type": "city"}, "id": "place:0454050", "key": "0454050", "kind": "place", "lat": 33.786186, "lon": -112.308042, "name": "Peoria", "rank": 5.3, "resolver": "place", "state": "AZ"} {"aliases": ["peridot", "peridot arizona", "peridot az"], "attrs": {"land_km2": 14.1, "pop": 0, "type": "cdp"}, "id": "place:0454190", "key": "0454190", "kind": "place", "lat": 33.303346, "lon": -110.459033, "name": "Peridot", "rank": 0.0, "resolver": "place", "state": "AZ"} -{"aliases": ["phoenix", "phoenix arizona", "phoenix az"], "attrs": {"land_km2": 1342.5, "pop": 1673164, "type": "city"}, "id": "place:0455000", "key": "0455000", "kind": "place", "lat": 33.572154, "lon": -112.090132, "name": "Phoenix", "rank": 6.22, "resolver": "place", "state": "AZ"} +{"aliases": ["phoenix", "phoenix arizona", "phoenix az", "phx"], "attrs": {"land_km2": 1342.5, "pop": 1673164, "type": "city"}, "id": "place:0455000", "key": "0455000", "kind": "place", "lat": 33.572154, "lon": -112.090132, "name": "Phoenix", "rank": 6.22, "resolver": "place", "state": "AZ"} {"aliases": ["picacho", "picacho arizona", "picacho az"], "attrs": {"land_km2": 15.8, "pop": 0, "type": "cdp"}, "id": "place:0455210", "key": "0455210", "kind": "place", "lat": 32.710196, "lon": -111.494296, "name": "Picacho", "rank": 0.0, "resolver": "place", "state": "AZ"} {"aliases": ["picture rocks", "picture rocks arizona", "picture rocks az"], "attrs": {"land_km2": 183.5, "pop": 0, "type": "cdp"}, "id": "place:0455300", "key": "0455300", "kind": "place", "lat": 32.324713, "lon": -111.259861, "name": "Picture Rocks", "rank": 0.0, "resolver": "place", "state": "AZ"} {"aliases": ["pima", "pima arizona", "pima az"], "attrs": {"land_km2": 18.9, "pop": 3148, "type": "town"}, "id": "place:0455560", "key": "0455560", "kind": "place", "lat": 32.891886, "lon": -109.844268, "name": "Pima", "rank": 3.5, "resolver": "place", "state": "AZ"} @@ -1752,7 +1752,7 @@ {"aliases": ["lexa", "lexa ar", "lexa arkansas"], "attrs": {"land_km2": 1.1, "pop": 180, "type": "town"}, "id": "place:0539670", "key": "0539670", "kind": "place", "lat": 34.598248, "lon": -90.751887, "name": "Lexa", "rank": 2.26, "resolver": "place", "state": "AR"} {"aliases": ["lincoln", "lincoln ar", "lincoln arkansas"], "attrs": {"land_km2": 7.4, "pop": 2371, "type": "city"}, "id": "place:0539970", "key": "0539970", "kind": "place", "lat": 35.948834, "lon": -94.417468, "name": "Lincoln", "rank": 3.37, "resolver": "place", "state": "AR"} {"aliases": ["little flock", "little flock ar", "little flock arkansas"], "attrs": {"land_km2": 19.5, "pop": 3040, "type": "city"}, "id": "place:0540120", "key": "0540120", "kind": "place", "lat": 36.383335, "lon": -94.137265, "name": "Little Flock", "rank": 3.48, "resolver": "place", "state": "AR"} -{"aliases": ["little rock", "little rock ar", "little rock arkansas"], "attrs": {"land_km2": 314.0, "pop": 204774, "type": "city"}, "id": "place:0541000", "key": "0541000", "kind": "place", "lat": 34.725393, "lon": -92.358556, "name": "Little Rock", "rank": 5.31, "resolver": "place", "state": "AR"} +{"aliases": ["little rock", "little rock ar", "little rock arkansas", "lr"], "attrs": {"land_km2": 314.0, "pop": 204774, "type": "city"}, "id": "place:0541000", "key": "0541000", "kind": "place", "lat": 34.725393, "lon": -92.358556, "name": "Little Rock", "rank": 5.31, "resolver": "place", "state": "AR"} {"aliases": ["lockesburg", "lockesburg ar", "lockesburg arkansas"], "attrs": {"land_km2": 9.0, "pop": 595, "type": "city"}, "id": "place:0541090", "key": "0541090", "kind": "place", "lat": 33.972871, "lon": -94.179584, "name": "Lockesburg", "rank": 2.77, "resolver": "place", "state": "AR"} {"aliases": ["london", "london ar", "london arkansas"], "attrs": {"land_km2": 8.2, "pop": 1041, "type": "city"}, "id": "place:0541270", "key": "0541270", "kind": "place", "lat": 35.327616, "lon": -93.243466, "name": "London", "rank": 3.02, "resolver": "place", "state": "AR"} {"aliases": ["lonoke", "lonoke ar", "lonoke arkansas"], "attrs": {"land_km2": 12.5, "pop": 4287, "type": "city"}, "id": "place:0541420", "key": "0541420", "kind": "place", "lat": 34.790501, "lon": -91.907362, "name": "Lonoke", "rank": 3.63, "resolver": "place", "state": "AR"} @@ -1833,7 +1833,7 @@ {"aliases": ["norman", "norman ar", "norman arkansas"], "attrs": {"land_km2": 2.9, "pop": 305, "type": "town"}, "id": "place:0550030", "key": "0550030", "kind": "place", "lat": 34.458977, "lon": -93.676824, "name": "Norman", "rank": 2.48, "resolver": "place", "state": "AR"} {"aliases": ["norphlet", "norphlet ar", "norphlet arkansas"], "attrs": {"land_km2": 5.3, "pop": 719, "type": "city"}, "id": "place:0550060", "key": "0550060", "kind": "place", "lat": 33.319889, "lon": -92.662009, "name": "Norphlet", "rank": 2.86, "resolver": "place", "state": "AR"} {"aliases": ["north crossett", "north crossett ar", "north crossett arkansas"], "attrs": {"land_km2": 27.0, "pop": 0, "type": "cdp"}, "id": "place:0550240", "key": "0550240", "kind": "place", "lat": 33.174113, "lon": -91.931736, "name": "North Crossett", "rank": 0.0, "resolver": "place", "state": "AR"} -{"aliases": ["north little rock", "north little rock ar", "north little rock arkansas"], "attrs": {"land_km2": 137.4, "pop": 64498, "type": "city"}, "id": "place:0550450", "key": "0550450", "kind": "place", "lat": 34.781752, "lon": -92.235057, "name": "North Little Rock", "rank": 4.81, "resolver": "place", "state": "AR"} +{"aliases": ["nlr", "north little rock", "north little rock ar", "north little rock arkansas"], "attrs": {"land_km2": 137.4, "pop": 64498, "type": "city"}, "id": "place:0550450", "key": "0550450", "kind": "place", "lat": 34.781752, "lon": -92.235057, "name": "North Little Rock", "rank": 4.81, "resolver": "place", "state": "AR"} {"aliases": ["oak grove", "oak grove ar", "oak grove arkansas"], "attrs": {"land_km2": 7.3, "pop": 393, "type": "town"}, "id": "place:0550810", "key": "0550810", "kind": "place", "lat": 36.470205, "lon": -93.42809, "name": "Oak Grove", "rank": 2.59, "resolver": "place", "state": "AR"} {"aliases": ["oak grove", "oak grove ar", "oak grove arkansas"], "attrs": {"land_km2": 9.7, "pop": 0, "type": "cdp"}, "id": "place:0550990", "key": "0550990", "kind": "place", "lat": 35.364779, "lon": -92.964179, "name": "Oak Grove", "rank": 0.0, "resolver": "place", "state": "AR"} {"aliases": ["oak grove heights", "oak grove heights ar", "oak grove heights arkansas"], "attrs": {"land_km2": 7.8, "pop": 1163, "type": "town"}, "id": "place:0551060", "key": "0551060", "kind": "place", "lat": 36.126432, "lon": -90.503435, "name": "Oak Grove Heights", "rank": 3.07, "resolver": "place", "state": "AR"} @@ -2079,7 +2079,7 @@ {"aliases": ["anaheim", "anaheim ca", "anaheim california"], "attrs": {"land_km2": 130.2, "pop": 344561, "type": "city"}, "id": "place:0602000", "key": "0602000", "kind": "place", "lat": 33.855502, "lon": -117.758657, "name": "Anaheim", "rank": 5.54, "resolver": "place", "state": "CA"} {"aliases": ["anchor bay", "anchor bay ca", "anchor bay california"], "attrs": {"land_km2": 9.1, "pop": 0, "type": "cdp"}, "id": "place:0602028", "key": "0602028", "kind": "place", "lat": 38.812653, "lon": -123.570267, "name": "Anchor Bay", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["anderson", "anderson ca", "anderson california"], "attrs": {"land_km2": 18.2, "pop": 11171, "type": "city"}, "id": "place:0602042", "key": "0602042", "kind": "place", "lat": 40.449755, "lon": -122.29507, "name": "Anderson", "rank": 4.05, "resolver": "place", "state": "CA"} -{"aliases": ["angels", "angels ca", "angels california"], "attrs": {"land_km2": 9.4, "pop": 3728, "type": "city"}, "id": "place:0602112", "key": "0602112", "kind": "place", "lat": 38.071037, "lon": -120.551729, "name": "Angels", "rank": 3.57, "resolver": "place", "state": "CA"} +{"aliases": ["angels", "angels ca", "angels california", "frogtown"], "attrs": {"land_km2": 9.4, "pop": 3728, "type": "city"}, "id": "place:0602112", "key": "0602112", "kind": "place", "lat": 38.071037, "lon": -120.551729, "name": "Angels", "rank": 3.57, "resolver": "place", "state": "CA"} {"aliases": ["angwin", "angwin ca", "angwin california"], "attrs": {"land_km2": 12.5, "pop": 0, "type": "cdp"}, "id": "place:0602168", "key": "0602168", "kind": "place", "lat": 38.577875, "lon": -122.451224, "name": "Angwin", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["antelope", "antelope ca", "antelope california"], "attrs": {"land_km2": 17.7, "pop": 0, "type": "cdp"}, "id": "place:0602210", "key": "0602210", "kind": "place", "lat": 38.71533, "lon": -121.360962, "name": "Antelope", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["antioch", "antioch ca", "antioch california"], "attrs": {"land_km2": 75.6, "pop": 118453, "type": "city"}, "id": "place:0602252", "key": "0602252", "kind": "place", "lat": 37.978336, "lon": -121.796064, "name": "Antioch", "rank": 5.07, "resolver": "place", "state": "CA"} @@ -2882,7 +2882,7 @@ {"aliases": ["los alamos", "los alamos ca", "los alamos california"], "attrs": {"land_km2": 2.3, "pop": 0, "type": "cdp"}, "id": "place:0643252", "key": "0643252", "kind": "place", "lat": 34.741803, "lon": -120.274596, "name": "Los Alamos", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["los altos", "los altos ca", "los altos california"], "attrs": {"land_km2": 16.9, "pop": 30864, "type": "city"}, "id": "place:0643280", "key": "0643280", "kind": "place", "lat": 37.368431, "lon": -122.09657, "name": "Los Altos", "rank": 4.49, "resolver": "place", "state": "CA"} {"aliases": ["los altos hills", "los altos hills ca", "los altos hills california"], "attrs": {"land_km2": 23.5, "pop": 8435, "type": "town"}, "id": "place:0643294", "key": "0643294", "kind": "place", "lat": 37.366808, "lon": -122.13865, "name": "Los Altos Hills", "rank": 3.93, "resolver": "place", "state": "CA"} -{"aliases": ["los angeles", "los angeles ca", "los angeles california"], "attrs": {"land_km2": 1218.6, "pop": 3878704, "type": "city"}, "id": "place:0644000", "key": "0644000", "kind": "place", "lat": 34.019394, "lon": -118.410825, "name": "Los Angeles", "rank": 6.59, "resolver": "place", "state": "CA"} +{"aliases": ["city of angels", "l a", "la", "la la land", "los angeles", "los angeles ca", "los angeles california", "tinseltown"], "attrs": {"land_km2": 1218.6, "pop": 3878704, "type": "city"}, "id": "place:0644000", "key": "0644000", "kind": "place", "lat": 34.019394, "lon": -118.410825, "name": "Los Angeles", "rank": 6.59, "resolver": "place", "state": "CA"} {"aliases": ["los banos", "los banos ca", "los banos california"], "attrs": {"land_km2": 25.9, "pop": 48684, "type": "city"}, "id": "place:0644028", "key": "0644028", "kind": "place", "lat": 37.063184, "lon": -120.840304, "name": "Los Banos", "rank": 4.69, "resolver": "place", "state": "CA"} {"aliases": ["los berros", "los berros ca", "los berros california"], "attrs": {"land_km2": 6.5, "pop": 0, "type": "cdp"}, "id": "place:0644042", "key": "0644042", "kind": "place", "lat": 35.080907, "lon": -120.545036, "name": "Los Berros", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["los gatos", "los gatos ca", "los gatos california"], "attrs": {"land_km2": 29.9, "pop": 32952, "type": "town"}, "id": "place:0644112", "key": "0644112", "kind": "place", "lat": 37.229944, "lon": -121.956874, "name": "Los Gatos", "rank": 4.52, "resolver": "place", "state": "CA"} @@ -3285,7 +3285,7 @@ {"aliases": ["running springs", "running springs ca", "running springs california"], "attrs": {"land_km2": 10.9, "pop": 0, "type": "cdp"}, "id": "place:0663316", "key": "0663316", "kind": "place", "lat": 34.211362, "lon": -117.104572, "name": "Running Springs", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["ruth", "ruth ca", "ruth california"], "attrs": {"land_km2": 100.7, "pop": 0, "type": "cdp"}, "id": "place:0663386", "key": "0663386", "kind": "place", "lat": 40.293849, "lon": -123.348335, "name": "Ruth", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["rutherford", "rutherford ca", "rutherford california"], "attrs": {"land_km2": 4.4, "pop": 0, "type": "cdp"}, "id": "place:0663400", "key": "0663400", "kind": "place", "lat": 38.458864, "lon": -122.428831, "name": "Rutherford", "rank": 0.0, "resolver": "place", "state": "CA"} -{"aliases": ["sacramento", "sacramento ca", "sacramento california"], "attrs": {"land_km2": 255.5, "pop": 535798, "type": "city"}, "id": "place:0664000", "key": "0664000", "kind": "place", "lat": 38.567694, "lon": -121.468161, "name": "Sacramento", "rank": 5.73, "resolver": "place", "state": "CA"} +{"aliases": ["sacramento", "sacramento ca", "sacramento california", "sactown"], "attrs": {"land_km2": 255.5, "pop": 535798, "type": "city"}, "id": "place:0664000", "key": "0664000", "kind": "place", "lat": 38.567694, "lon": -121.468161, "name": "Sacramento", "rank": 5.73, "resolver": "place", "state": "CA"} {"aliases": ["sage", "sage ca", "sage california"], "attrs": {"land_km2": 176.8, "pop": 0, "type": "cdp"}, "id": "place:0664056", "key": "0664056", "kind": "place", "lat": 33.594066, "lon": -116.910385, "name": "Sage", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["st helena", "st helena ca", "st helena california"], "attrs": {"land_km2": 12.8, "pop": 5257, "type": "city"}, "id": "place:0664140", "key": "0664140", "kind": "place", "lat": 38.512806, "lon": -122.468076, "name": "St. Helena", "rank": 3.72, "resolver": "place", "state": "CA"} {"aliases": ["salida", "salida ca", "salida california"], "attrs": {"land_km2": 5.7, "pop": 0, "type": "cdp"}, "id": "place:0664210", "key": "0664210", "kind": "place", "lat": 37.709123, "lon": -121.084825, "name": "Salida", "rank": 0.0, "resolver": "place", "state": "CA"} @@ -3309,7 +3309,7 @@ {"aliases": ["san diego country estates", "san diego country estates ca", "san diego country estates california"], "attrs": {"land_km2": 43.5, "pop": 0, "type": "cdp"}, "id": "place:0666004", "key": "0666004", "kind": "place", "lat": 33.009376, "lon": -116.787314, "name": "San Diego Country Estates", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["san dimas", "san dimas ca", "san dimas california"], "attrs": {"land_km2": 38.9, "pop": 33226, "type": "city"}, "id": "place:0666070", "key": "0666070", "kind": "place", "lat": 34.107957, "lon": -117.808529, "name": "San Dimas", "rank": 4.52, "resolver": "place", "state": "CA"} {"aliases": ["san fernando", "san fernando ca", "san fernando california"], "attrs": {"land_km2": 6.1, "pop": 23488, "type": "city"}, "id": "place:0666140", "key": "0666140", "kind": "place", "lat": 34.288652, "lon": -118.436243, "name": "San Fernando", "rank": 4.37, "resolver": "place", "state": "CA"} -{"aliases": ["san francisco", "san francisco ca", "san francisco california"], "attrs": {"land_km2": 120.9, "pop": 827526, "type": "city"}, "id": "place:0667000", "key": "0667000", "kind": "place", "lat": 37.727239, "lon": -123.032229, "name": "San Francisco", "rank": 5.92, "resolver": "place", "state": "CA"} +{"aliases": ["san fran", "san francisco", "san francisco ca", "san francisco california", "sf"], "attrs": {"land_km2": 120.9, "pop": 827526, "type": "city"}, "id": "place:0667000", "key": "0667000", "kind": "place", "lat": 37.727239, "lon": -123.032229, "name": "San Francisco", "rank": 5.92, "resolver": "place", "state": "CA"} {"aliases": ["san gabriel", "san gabriel ca", "san gabriel california"], "attrs": {"land_km2": 10.7, "pop": 37926, "type": "city"}, "id": "place:0667042", "key": "0667042", "kind": "place", "lat": 34.094636, "lon": -118.098469, "name": "San Gabriel", "rank": 4.58, "resolver": "place", "state": "CA"} {"aliases": ["sanger", "sanger ca", "sanger california"], "attrs": {"land_km2": 16.1, "pop": 26649, "type": "city"}, "id": "place:0667056", "key": "0667056", "kind": "place", "lat": 36.701219, "lon": -119.557396, "name": "Sanger", "rank": 4.43, "resolver": "place", "state": "CA"} {"aliases": ["san geronimo", "san geronimo ca", "san geronimo california"], "attrs": {"land_km2": 3.9, "pop": 0, "type": "cdp"}, "id": "place:0667070", "key": "0667070", "kind": "place", "lat": 38.006713, "lon": -122.663266, "name": "San Geronimo", "rank": 0.0, "resolver": "place", "state": "CA"} @@ -3592,7 +3592,7 @@ {"aliases": ["west covina", "west covina ca", "west covina california"], "attrs": {"land_km2": 41.5, "pop": 106920, "type": "city"}, "id": "place:0684200", "key": "0684200", "kind": "place", "lat": 34.055941, "lon": -117.909937, "name": "West Covina", "rank": 5.03, "resolver": "place", "state": "CA"} {"aliases": ["west goshen", "west goshen ca", "west goshen california"], "attrs": {"land_km2": 1.7, "pop": 0, "type": "cdp"}, "id": "place:0684345", "key": "0684345", "kind": "place", "lat": 36.349183, "lon": -119.457374, "name": "West Goshen", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["westhaven", "westhaven ca", "westhaven california", "westhaven moonstone", "westhaven moonstone ca", "westhaven moonstone california"], "attrs": {"land_km2": 21.0, "pop": 0, "type": "cdp"}, "id": "place:0684385", "key": "0684385", "kind": "place", "lat": 41.036117, "lon": -124.09083, "name": "Westhaven-Moonstone", "rank": 0.0, "resolver": "place", "state": "CA"} -{"aliases": ["west hollywood", "west hollywood ca", "west hollywood california"], "attrs": {"land_km2": 4.9, "pop": 34371, "type": "city"}, "id": "place:0684410", "key": "0684410", "kind": "place", "lat": 34.088268, "lon": -118.371831, "name": "West Hollywood", "rank": 4.54, "resolver": "place", "state": "CA"} +{"aliases": ["boystown", "west hollywood", "west hollywood ca", "west hollywood california"], "attrs": {"land_km2": 4.9, "pop": 34371, "type": "city"}, "id": "place:0684410", "key": "0684410", "kind": "place", "lat": 34.088268, "lon": -118.371831, "name": "West Hollywood", "rank": 4.54, "resolver": "place", "state": "CA"} {"aliases": ["westlake village", "westlake village ca", "westlake village california"], "attrs": {"land_km2": 13.4, "pop": 7593, "type": "city"}, "id": "place:0684438", "key": "0684438", "kind": "place", "lat": 34.136675, "lon": -118.817367, "name": "Westlake Village", "rank": 3.88, "resolver": "place", "state": "CA"} {"aliases": ["westley", "westley ca", "westley california"], "attrs": {"land_km2": 0.9, "pop": 0, "type": "cdp"}, "id": "place:0684480", "key": "0684480", "kind": "place", "lat": 37.546908, "lon": -121.200915, "name": "Westley", "rank": 0.0, "resolver": "place", "state": "CA"} {"aliases": ["west menlo park", "west menlo park ca", "west menlo park california"], "attrs": {"land_km2": 1.3, "pop": 0, "type": "cdp"}, "id": "place:0684536", "key": "0684536", "kind": "place", "lat": 37.433824, "lon": -122.20342, "name": "West Menlo Park", "rank": 0.0, "resolver": "place", "state": "CA"} @@ -3773,7 +3773,7 @@ {"aliases": ["deer trail", "deer trail co", "deer trail colorado"], "attrs": {"land_km2": 3.3, "pop": 1624, "type": "town"}, "id": "place:0819630", "key": "0819630", "kind": "place", "lat": 39.617, "lon": -104.043851, "name": "Deer Trail", "rank": 3.21, "resolver": "place", "state": "CO"} {"aliases": ["del norte", "del norte co", "del norte colorado"], "attrs": {"land_km2": 2.8, "pop": 1410, "type": "town"}, "id": "place:0819795", "key": "0819795", "kind": "place", "lat": 37.677975, "lon": -106.353018, "name": "Del Norte", "rank": 3.15, "resolver": "place", "state": "CO"} {"aliases": ["delta", "delta co", "delta colorado"], "attrs": {"land_km2": 34.4, "pop": 9688, "type": "city"}, "id": "place:0819850", "key": "0819850", "kind": "place", "lat": 38.757371, "lon": -108.087986, "name": "Delta", "rank": 3.99, "resolver": "place", "state": "CO"} -{"aliases": ["denver", "denver co", "denver colorado"], "attrs": {"land_km2": 396.5, "pop": 729019, "type": "city"}, "id": "place:0820000", "key": "0820000", "kind": "place", "lat": 39.76185, "lon": -104.881105, "name": "Denver", "rank": 5.86, "resolver": "place", "state": "CO"} +{"aliases": ["denver", "denver co", "denver colorado", "mile high city"], "attrs": {"land_km2": 396.5, "pop": 729019, "type": "city"}, "id": "place:0820000", "key": "0820000", "kind": "place", "lat": 39.76185, "lon": -104.881105, "name": "Denver", "rank": 5.86, "resolver": "place", "state": "CO"} {"aliases": ["derby", "derby co", "derby colorado"], "attrs": {"land_km2": 4.5, "pop": 0, "type": "cdp"}, "id": "place:0820275", "key": "0820275", "kind": "place", "lat": 39.840791, "lon": -104.916254, "name": "Derby", "rank": 0.0, "resolver": "place", "state": "CO"} {"aliases": ["dillon", "dillon co", "dillon colorado"], "attrs": {"land_km2": 3.8, "pop": 1013, "type": "town"}, "id": "place:0820440", "key": "0820440", "kind": "place", "lat": 39.625881, "lon": -106.043728, "name": "Dillon", "rank": 3.01, "resolver": "place", "state": "CO"} {"aliases": ["dinosaur", "dinosaur co", "dinosaur colorado"], "attrs": {"land_km2": 2.4, "pop": 247, "type": "town"}, "id": "place:0820495", "key": "0820495", "kind": "place", "lat": 40.239861, "lon": -109.006795, "name": "Dinosaur", "rank": 2.39, "resolver": "place", "state": "CO"} @@ -4293,7 +4293,7 @@ {"aliases": ["riverside", "riverside connecticut", "riverside ct"], "attrs": {"land_km2": 6.3, "pop": 0, "type": "cdp"}, "id": "place:0964530", "key": "0964530", "kind": "place", "lat": 41.030093, "lon": -73.583719, "name": "Riverside", "rank": 0.0, "resolver": "place", "state": "CT"} {"aliases": ["riverton", "riverton connecticut", "riverton ct"], "attrs": {"land_km2": 1.3, "pop": 0, "type": "cdp"}, "id": "place:0964880", "key": "0964880", "kind": "place", "lat": 41.963141, "lon": -73.016558, "name": "Riverton", "rank": 0.0, "resolver": "place", "state": "CT"} {"aliases": ["rock ridge", "rock ridge connecticut", "rock ridge ct"], "attrs": {"land_km2": 2.4, "pop": 0, "type": "cdp"}, "id": "place:0965160", "key": "0965160", "kind": "place", "lat": 41.03442, "lon": -73.648396, "name": "Rock Ridge", "rank": 0.0, "resolver": "place", "state": "CT"} -{"aliases": ["rockville", "rockville connecticut", "rockville ct"], "attrs": {"land_km2": 4.8, "pop": 0, "type": "cdp"}, "id": "place:0965230", "key": "0965230", "kind": "place", "lat": 41.866519, "lon": -72.45127, "name": "Rockville", "rank": 0.0, "resolver": "place", "state": "CT"} +{"aliases": ["loom city", "rockville", "rockville connecticut", "rockville ct"], "attrs": {"land_km2": 4.8, "pop": 0, "type": "cdp"}, "id": "place:0965230", "key": "0965230", "kind": "place", "lat": 41.866519, "lon": -72.45127, "name": "Rockville", "rank": 0.0, "resolver": "place", "state": "CT"} {"aliases": ["route 7 gateway", "route 7 gateway connecticut", "route 7 gateway ct"], "attrs": {"land_km2": 1.8, "pop": 0, "type": "cdp"}, "id": "place:0965685", "key": "0965685", "kind": "place", "lat": 41.318764, "lon": -73.475382, "name": "Route 7 Gateway", "rank": 0.0, "resolver": "place", "state": "CT"} {"aliases": ["sacred heart university", "sacred heart university connecticut", "sacred heart university ct"], "attrs": {"land_km2": 0.3, "pop": 0, "type": "cdp"}, "id": "place:0966086", "key": "0966086", "kind": "place", "lat": 41.22148, "lon": -73.2442, "name": "Sacred Heart University", "rank": 0.0, "resolver": "place", "state": "CT"} {"aliases": ["sail harbor", "sail harbor connecticut", "sail harbor ct"], "attrs": {"land_km2": 1.2, "pop": 0, "type": "cdp"}, "id": "place:0966113", "key": "0966113", "kind": "place", "lat": 41.52419, "lon": -73.464218, "name": "Sail Harbor", "rank": 0.0, "resolver": "place", "state": "CT"} @@ -4433,7 +4433,7 @@ {"aliases": ["woodside", "woodside de", "woodside delaware"], "attrs": {"land_km2": 0.4, "pop": 198, "type": "town"}, "id": "place:1080830", "key": "1080830", "kind": "place", "lat": 39.071086, "lon": -75.567724, "name": "Woodside", "rank": 2.3, "resolver": "place", "state": "DE"} {"aliases": ["woodside east", "woodside east de", "woodside east delaware"], "attrs": {"land_km2": 4.4, "pop": 0, "type": "cdp"}, "id": "place:1080895", "key": "1080895", "kind": "place", "lat": 39.067544, "lon": -75.537625, "name": "Woodside East", "rank": 0.0, "resolver": "place", "state": "DE"} {"aliases": ["wyoming", "wyoming de", "wyoming delaware"], "attrs": {"land_km2": 2.7, "pop": 1871, "type": "town"}, "id": "place:1081350", "key": "1081350", "kind": "place", "lat": 39.112407, "lon": -75.564916, "name": "Wyoming", "rank": 3.27, "resolver": "place", "state": "DE"} -{"aliases": ["washington", "washington dc", "washington district of columbia"], "attrs": {"land_km2": 158.3, "pop": 702250, "type": "city"}, "id": "place:1150000", "key": "1150000", "kind": "place", "lat": 38.904247, "lon": -77.016517, "name": "Washington", "rank": 5.85, "resolver": "place", "state": "DC"} +{"aliases": ["chocolate city", "d c", "dc", "washington", "washington dc", "washington district of columbia"], "attrs": {"land_km2": 158.3, "pop": 702250, "type": "city"}, "id": "place:1150000", "key": "1150000", "kind": "place", "lat": 38.904247, "lon": -77.016517, "name": "Washington", "rank": 5.85, "resolver": "place", "state": "DC"} {"aliases": ["acacia villas", "acacia villas fl", "acacia villas florida"], "attrs": {"land_km2": 0.2, "pop": 0, "type": "cdp"}, "id": "place:1200075", "key": "1200075", "kind": "place", "lat": 26.645735, "lon": -80.110444, "name": "Acacia Villas", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["alachua", "alachua fl", "alachua florida"], "attrs": {"land_km2": 93.7, "pop": 10856, "type": "city"}, "id": "place:1200375", "key": "1200375", "kind": "place", "lat": 29.765199, "lon": -82.4751, "name": "Alachua", "rank": 4.04, "resolver": "place", "state": "FL"} {"aliases": ["alafaya", "alafaya fl", "alafaya florida"], "attrs": {"land_km2": 79.3, "pop": 0, "type": "cdp"}, "id": "place:1200410", "key": "1200410", "kind": "place", "lat": 28.528373, "lon": -81.185745, "name": "Alafaya", "rank": 0.0, "resolver": "place", "state": "FL"} @@ -4683,7 +4683,7 @@ {"aliases": ["fort denaud", "fort denaud fl", "fort denaud florida"], "attrs": {"land_km2": 51.9, "pop": 0, "type": "cdp"}, "id": "place:1223730", "key": "1223730", "kind": "place", "lat": 26.72271, "lon": -81.554106, "name": "Fort Denaud", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["fort green", "fort green fl", "fort green florida"], "attrs": {"land_km2": 10.5, "pop": 0, "type": "cdp"}, "id": "place:1223850", "key": "1223850", "kind": "place", "lat": 27.625455, "lon": -81.939711, "name": "Fort Green", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["fort green springs", "fort green springs fl", "fort green springs florida"], "attrs": {"land_km2": 26.9, "pop": 0, "type": "cdp"}, "id": "place:1223875", "key": "1223875", "kind": "place", "lat": 27.587623, "lon": -81.940198, "name": "Fort Green Springs", "rank": 0.0, "resolver": "place", "state": "FL"} -{"aliases": ["fort lauderdale", "fort lauderdale fl", "fort lauderdale florida"], "attrs": {"land_km2": 89.6, "pop": 190641, "type": "city"}, "id": "place:1224000", "key": "1224000", "kind": "place", "lat": 26.141227, "lon": -80.146731, "name": "Fort Lauderdale", "rank": 5.28, "resolver": "place", "state": "FL"} +{"aliases": ["fort lauderdale", "fort lauderdale fl", "fort lauderdale florida", "ft lauderdale"], "attrs": {"land_km2": 89.6, "pop": 190641, "type": "city"}, "id": "place:1224000", "key": "1224000", "kind": "place", "lat": 26.141227, "lon": -80.146731, "name": "Fort Lauderdale", "rank": 5.28, "resolver": "place", "state": "FL"} {"aliases": ["fort meade", "fort meade fl", "fort meade florida"], "attrs": {"land_km2": 21.2, "pop": 5341, "type": "city"}, "id": "place:1224100", "key": "1224100", "kind": "place", "lat": 27.765879, "lon": -81.805464, "name": "Fort Meade", "rank": 3.73, "resolver": "place", "state": "FL"} {"aliases": ["fort myers", "fort myers fl", "fort myers florida"], "attrs": {"land_km2": 103.7, "pop": 99918, "type": "city"}, "id": "place:1224125", "key": "1224125", "kind": "place", "lat": 26.61982, "lon": -81.831673, "name": "Fort Myers", "rank": 5.0, "resolver": "place", "state": "FL"} {"aliases": ["fort myers beach", "fort myers beach fl", "fort myers beach florida"], "attrs": {"land_km2": 7.2, "pop": 5308, "type": "town"}, "id": "place:1224150", "key": "1224150", "kind": "place", "lat": 26.430176, "lon": -81.91404, "name": "Fort Myers Beach", "rank": 3.72, "resolver": "place", "state": "FL"} @@ -4815,7 +4815,7 @@ {"aliases": ["island walk", "island walk fl", "island walk florida"], "attrs": {"land_km2": 2.6, "pop": 0, "type": "cdp"}, "id": "place:1234185", "key": "1234185", "kind": "place", "lat": 26.25103, "lon": -81.710982, "name": "Island Walk", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["istachatta", "istachatta fl", "istachatta florida"], "attrs": {"land_km2": 0.8, "pop": 0, "type": "cdp"}, "id": "place:1234275", "key": "1234275", "kind": "place", "lat": 28.661859, "lon": -82.274532, "name": "Istachatta", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["ives estates", "ives estates fl", "ives estates florida"], "attrs": {"land_km2": 6.6, "pop": 0, "type": "cdp"}, "id": "place:1234400", "key": "1234400", "kind": "place", "lat": 25.963406, "lon": -80.182871, "name": "Ives Estates", "rank": 0.0, "resolver": "place", "state": "FL"} -{"aliases": ["jacksonville", "jacksonville fl", "jacksonville florida"], "attrs": {"land_km2": 1935.4, "pop": 1009833, "type": "city"}, "id": "place:1235000", "key": "1235000", "kind": "place", "lat": 30.336864, "lon": -81.661603, "name": "Jacksonville", "rank": 6.0, "resolver": "place", "state": "FL"} +{"aliases": ["jacksonville", "jacksonville fl", "jacksonville florida", "jax"], "attrs": {"land_km2": 1935.4, "pop": 1009833, "type": "city"}, "id": "place:1235000", "key": "1235000", "kind": "place", "lat": 30.336864, "lon": -81.661603, "name": "Jacksonville", "rank": 6.0, "resolver": "place", "state": "FL"} {"aliases": ["jacksonville beach", "jacksonville beach fl", "jacksonville beach florida"], "attrs": {"land_km2": 18.9, "pop": 23630, "type": "city"}, "id": "place:1235050", "key": "1235050", "kind": "place", "lat": 30.272466, "lon": -81.385604, "name": "Jacksonville Beach", "rank": 4.37, "resolver": "place", "state": "FL"} {"aliases": ["jacob", "jacob city", "jacob city fl", "jacob city florida", "jacob fl", "jacob florida"], "attrs": {"land_km2": 7.9, "pop": 230, "type": "city"}, "id": "place:1235200", "key": "1235200", "kind": "place", "lat": 30.885534, "lon": -85.413569, "name": "Jacob City", "rank": 2.36, "resolver": "place", "state": "FL"} {"aliases": ["jan phyl village", "jan phyl village fl", "jan phyl village florida"], "attrs": {"land_km2": 11.8, "pop": 0, "type": "cdp"}, "id": "place:1235300", "key": "1235300", "kind": "place", "lat": 28.016212, "lon": -81.793764, "name": "Jan Phyl Village", "rank": 0.0, "resolver": "place", "state": "FL"} @@ -4955,7 +4955,7 @@ {"aliases": ["memphis", "memphis fl", "memphis florida"], "attrs": {"land_km2": 8.4, "pop": 0, "type": "cdp"}, "id": "place:1244175", "key": "1244175", "kind": "place", "lat": 27.54495, "lon": -82.555899, "name": "Memphis", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["merritt island", "merritt island fl", "merritt island florida"], "attrs": {"land_km2": 44.0, "pop": 0, "type": "cdp"}, "id": "place:1244275", "key": "1244275", "kind": "place", "lat": 28.309239, "lon": -80.675956, "name": "Merritt Island", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["mexico beach", "mexico beach fl", "mexico beach florida"], "attrs": {"land_km2": 6.3, "pop": 1210, "type": "city"}, "id": "place:1244300", "key": "1244300", "kind": "place", "lat": 29.945923, "lon": -85.412254, "name": "Mexico Beach", "rank": 3.08, "resolver": "place", "state": "FL"} -{"aliases": ["miami", "miami fl", "miami florida"], "attrs": {"land_km2": 93.2, "pop": 487014, "type": "city"}, "id": "place:1245000", "key": "1245000", "kind": "place", "lat": 25.775163, "lon": -80.208615, "name": "Miami", "rank": 5.69, "resolver": "place", "state": "FL"} +{"aliases": ["magic city", "miami", "miami fl", "miami florida"], "attrs": {"land_km2": 93.2, "pop": 487014, "type": "city"}, "id": "place:1245000", "key": "1245000", "kind": "place", "lat": 25.775163, "lon": -80.208615, "name": "Miami", "rank": 5.69, "resolver": "place", "state": "FL"} {"aliases": ["miami beach", "miami beach fl", "miami beach florida"], "attrs": {"land_km2": 19.9, "pop": 83251, "type": "city"}, "id": "place:1245025", "key": "1245025", "kind": "place", "lat": 25.810596, "lon": -80.148852, "name": "Miami Beach", "rank": 4.92, "resolver": "place", "state": "FL"} {"aliases": ["miami gardens", "miami gardens fl", "miami gardens florida"], "attrs": {"land_km2": 47.9, "pop": 116173, "type": "city"}, "id": "place:1245060", "key": "1245060", "kind": "place", "lat": 25.948892, "lon": -80.243585, "name": "Miami Gardens", "rank": 5.07, "resolver": "place", "state": "FL"} {"aliases": ["miami lakes", "miami lakes fl", "miami lakes florida"], "attrs": {"land_km2": 14.7, "pop": 33247, "type": "town"}, "id": "place:1245100", "key": "1245100", "kind": "place", "lat": 25.912739, "lon": -80.320422, "name": "Miami Lakes", "rank": 4.52, "resolver": "place", "state": "FL"} @@ -5048,7 +5048,7 @@ {"aliases": ["orangetree", "orangetree fl", "orangetree florida"], "attrs": {"land_km2": 9.4, "pop": 0, "type": "cdp"}, "id": "place:1252162", "key": "1252162", "kind": "place", "lat": 26.292899, "lon": -81.57587, "name": "Orangetree", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["orchid", "orchid fl", "orchid florida"], "attrs": {"land_km2": 3.2, "pop": 545, "type": "town"}, "id": "place:1252175", "key": "1252175", "kind": "place", "lat": 27.771941, "lon": -80.424461, "name": "Orchid", "rank": 2.74, "resolver": "place", "state": "FL"} {"aliases": ["oriole beach", "oriole beach fl", "oriole beach florida"], "attrs": {"land_km2": 2.2, "pop": 0, "type": "cdp"}, "id": "place:1252250", "key": "1252250", "kind": "place", "lat": 30.372465, "lon": -87.100486, "name": "Oriole Beach", "rank": 0.0, "resolver": "place", "state": "FL"} -{"aliases": ["orlando", "orlando fl", "orlando florida"], "attrs": {"land_km2": 287.8, "pop": 334854, "type": "city"}, "id": "place:1253000", "key": "1253000", "kind": "place", "lat": 28.40865, "lon": -81.254805, "name": "Orlando", "rank": 5.52, "resolver": "place", "state": "FL"} +{"aliases": ["city beautiful", "orlando", "orlando fl", "orlando florida"], "attrs": {"land_km2": 287.8, "pop": 334854, "type": "city"}, "id": "place:1253000", "key": "1253000", "kind": "place", "lat": 28.40865, "lon": -81.254805, "name": "Orlando", "rank": 5.52, "resolver": "place", "state": "FL"} {"aliases": ["orlovista", "orlovista fl", "orlovista florida"], "attrs": {"land_km2": 4.8, "pop": 0, "type": "cdp"}, "id": "place:1253100", "key": "1253100", "kind": "place", "lat": 28.544055, "lon": -81.462937, "name": "Orlovista", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["ormond beach", "ormond beach fl", "ormond beach florida"], "attrs": {"land_km2": 91.0, "pop": 44596, "type": "city"}, "id": "place:1253150", "key": "1253150", "kind": "place", "lat": 29.293276, "lon": -81.101447, "name": "Ormond Beach", "rank": 4.65, "resolver": "place", "state": "FL"} {"aliases": ["ormond", "ormond by the sea", "ormond by the sea fl", "ormond by the sea florida", "ormond fl", "ormond florida"], "attrs": {"land_km2": 5.2, "pop": 0, "type": "cdp"}, "id": "place:1253200", "key": "1253200", "kind": "place", "lat": 29.343284, "lon": -81.068605, "name": "Ormond-by-the-Sea", "rank": 0.0, "resolver": "place", "state": "FL"} @@ -5180,7 +5180,7 @@ {"aliases": ["st lucie village", "st lucie village fl", "st lucie village florida"], "attrs": {"land_km2": 2.3, "pop": 615, "type": "town"}, "id": "place:1262800", "key": "1262800", "kind": "place", "lat": 27.499881, "lon": -80.341812, "name": "St. Lucie Village", "rank": 2.79, "resolver": "place", "state": "FL"} {"aliases": ["st marks", "st marks fl", "st marks florida"], "attrs": {"land_km2": 5.1, "pop": 312, "type": "city"}, "id": "place:1262825", "key": "1262825", "kind": "place", "lat": 30.166095, "lon": -84.208292, "name": "St. Marks", "rank": 2.49, "resolver": "place", "state": "FL"} {"aliases": ["st pete beach", "st pete beach fl", "st pete beach florida"], "attrs": {"land_km2": 5.5, "pop": 8730, "type": "city"}, "id": "place:1262885", "key": "1262885", "kind": "place", "lat": 27.716801, "lon": -82.739622, "name": "St. Pete Beach", "rank": 3.94, "resolver": "place", "state": "FL"} -{"aliases": ["st petersburg", "st petersburg fl", "st petersburg florida"], "attrs": {"land_km2": 160.1, "pop": 267102, "type": "city"}, "id": "place:1263000", "key": "1263000", "kind": "place", "lat": 27.762727, "lon": -82.644131, "name": "St. Petersburg", "rank": 5.43, "resolver": "place", "state": "FL"} +{"aliases": ["st pete", "st petersburg", "st petersburg fl", "st petersburg florida"], "attrs": {"land_km2": 160.1, "pop": 267102, "type": "city"}, "id": "place:1263000", "key": "1263000", "kind": "place", "lat": 27.762727, "lon": -82.644131, "name": "St. Petersburg", "rank": 5.43, "resolver": "place", "state": "FL"} {"aliases": ["samoset", "samoset fl", "samoset florida"], "attrs": {"land_km2": 3.8, "pop": 0, "type": "cdp"}, "id": "place:1263225", "key": "1263225", "kind": "place", "lat": 27.476377, "lon": -82.543062, "name": "Samoset", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["samsula", "samsula fl", "samsula florida", "samsula spruce creek", "samsula spruce creek fl", "samsula spruce creek florida"], "attrs": {"land_km2": 43.5, "pop": 0, "type": "cdp"}, "id": "place:1263362", "key": "1263362", "kind": "place", "lat": 29.051647, "lon": -81.062493, "name": "Samsula-Spruce Creek", "rank": 0.0, "resolver": "place", "state": "FL"} {"aliases": ["san antonio", "san antonio fl", "san antonio florida"], "attrs": {"land_km2": 3.6, "pop": 1577, "type": "city"}, "id": "place:1263375", "key": "1263375", "kind": "place", "lat": 28.339858, "lon": -82.278605, "name": "San Antonio", "rank": 3.2, "resolver": "place", "state": "FL"} @@ -5419,7 +5419,7 @@ {"aliases": ["arnoldsville", "arnoldsville ga", "arnoldsville georgia"], "attrs": {"land_km2": 4.4, "pop": 469, "type": "city"}, "id": "place:1303124", "key": "1303124", "kind": "place", "lat": 33.914955, "lon": -83.218427, "name": "Arnoldsville", "rank": 2.67, "resolver": "place", "state": "GA"} {"aliases": ["ashburn", "ashburn ga", "ashburn georgia"], "attrs": {"land_km2": 12.3, "pop": 4239, "type": "city"}, "id": "place:1303236", "key": "1303236", "kind": "place", "lat": 31.709191, "lon": -83.652424, "name": "Ashburn", "rank": 3.63, "resolver": "place", "state": "GA"} {"aliases": ["athens", "athens clarke county", "athens clarke county ga", "athens clarke county georgia", "athens ga", "athens georgia"], "attrs": {"land_km2": 301.6, "pop": 128691, "type": "unified government (balance)"}, "id": "place:1303440", "key": "1303440", "kind": "place", "lat": 33.949597, "lon": -83.370088, "name": "Athens-Clarke County", "rank": 5.11, "resolver": "place", "state": "GA"} -{"aliases": ["atlanta", "atlanta ga", "atlanta georgia"], "attrs": {"land_km2": 350.4, "pop": 520070, "type": "city"}, "id": "place:1304000", "key": "1304000", "kind": "place", "lat": 33.762909, "lon": -84.422675, "name": "Atlanta", "rank": 5.72, "resolver": "place", "state": "GA"} +{"aliases": ["atl", "atlanta", "atlanta ga", "atlanta georgia", "big peach", "hotlanta"], "attrs": {"land_km2": 350.4, "pop": 520070, "type": "city"}, "id": "place:1304000", "key": "1304000", "kind": "place", "lat": 33.762909, "lon": -84.422675, "name": "Atlanta", "rank": 5.72, "resolver": "place", "state": "GA"} {"aliases": ["attapulgus", "attapulgus ga", "attapulgus georgia"], "attrs": {"land_km2": 2.1, "pop": 446, "type": "city"}, "id": "place:1304084", "key": "1304084", "kind": "place", "lat": 30.74949, "lon": -84.483886, "name": "Attapulgus", "rank": 2.65, "resolver": "place", "state": "GA"} {"aliases": ["auburn", "auburn ga", "auburn georgia"], "attrs": {"land_km2": 17.5, "pop": 10094, "type": "city"}, "id": "place:1304140", "key": "1304140", "kind": "place", "lat": 34.015186, "lon": -83.833741, "name": "Auburn", "rank": 4.0, "resolver": "place", "state": "GA"} {"aliases": ["augusta", "augusta ga", "augusta georgia", "augusta richmond county", "augusta richmond county ga", "augusta richmond county georgia"], "attrs": {"land_km2": 782.9, "pop": 201737, "type": "consolidated government (balance)"}, "id": "place:1304204", "key": "1304204", "kind": "place", "lat": 33.365531, "lon": -82.073422, "name": "Augusta-Richmond County", "rank": 5.3, "resolver": "place", "state": "GA"} @@ -6711,7 +6711,7 @@ {"aliases": ["chester", "chester il", "chester illinois"], "attrs": {"land_km2": 14.9, "pop": 6759, "type": "city"}, "id": "place:1713139", "key": "1713139", "kind": "place", "lat": 37.925757, "lon": -89.827704, "name": "Chester", "rank": 3.83, "resolver": "place", "state": "IL"} {"aliases": ["chesterfield", "chesterfield il", "chesterfield illinois"], "attrs": {"land_km2": 1.4, "pop": 165, "type": "village"}, "id": "place:1713165", "key": "1713165", "kind": "place", "lat": 39.256614, "lon": -90.066765, "name": "Chesterfield", "rank": 2.22, "resolver": "place", "state": "IL"} {"aliases": ["chestnut", "chestnut il", "chestnut illinois"], "attrs": {"land_km2": 0.3, "pop": 0, "type": "cdp"}, "id": "place:1713230", "key": "1713230", "kind": "place", "lat": 40.053734, "lon": -89.187111, "name": "Chestnut", "rank": 0.0, "resolver": "place", "state": "IL"} -{"aliases": ["chicago", "chicago il", "chicago illinois"], "attrs": {"land_km2": 589.9, "pop": 2721308, "type": "city"}, "id": "place:1714000", "key": "1714000", "kind": "place", "lat": 41.837045, "lon": -87.684939, "name": "Chicago", "rank": 6.43, "resolver": "place", "state": "IL"} +{"aliases": ["chi town", "chicago", "chicago il", "chicago illinois", "chitown", "windy city"], "attrs": {"land_km2": 589.9, "pop": 2721308, "type": "city"}, "id": "place:1714000", "key": "1714000", "kind": "place", "lat": 41.837045, "lon": -87.684939, "name": "Chicago", "rank": 6.43, "resolver": "place", "state": "IL"} {"aliases": ["chicago heights", "chicago heights il", "chicago heights illinois"], "attrs": {"land_km2": 26.9, "pop": 26533, "type": "city"}, "id": "place:1714026", "key": "1714026", "kind": "place", "lat": 41.510684, "lon": -87.634069, "name": "Chicago Heights", "rank": 4.42, "resolver": "place", "state": "IL"} {"aliases": ["chicago ridge", "chicago ridge il", "chicago ridge illinois"], "attrs": {"land_km2": 5.9, "pop": 14036, "type": "village"}, "id": "place:1714065", "key": "1714065", "kind": "place", "lat": 41.703415, "lon": -87.779467, "name": "Chicago Ridge", "rank": 4.15, "resolver": "place", "state": "IL"} {"aliases": ["chillicothe", "chillicothe il", "chillicothe illinois"], "attrs": {"land_km2": 13.7, "pop": 6052, "type": "city"}, "id": "place:1714117", "key": "1714117", "kind": "place", "lat": 40.915961, "lon": -89.50268, "name": "Chillicothe", "rank": 3.78, "resolver": "place", "state": "IL"} @@ -7552,7 +7552,7 @@ {"aliases": ["prophetstown", "prophetstown il", "prophetstown illinois"], "attrs": {"land_km2": 3.5, "pop": 1906, "type": "city"}, "id": "place:1761977", "key": "1761977", "kind": "place", "lat": 41.669656, "lon": -89.934681, "name": "Prophetstown", "rank": 3.28, "resolver": "place", "state": "IL"} {"aliases": ["prospect heights", "prospect heights il", "prospect heights illinois"], "attrs": {"land_km2": 10.9, "pop": 15566, "type": "city"}, "id": "place:1762016", "key": "1762016", "kind": "place", "lat": 42.103663, "lon": -87.928972, "name": "Prospect Heights", "rank": 4.19, "resolver": "place", "state": "IL"} {"aliases": ["pulaski", "pulaski il", "pulaski illinois"], "attrs": {"land_km2": 3.4, "pop": 143, "type": "village"}, "id": "place:1762211", "key": "1762211", "kind": "place", "lat": 37.220853, "lon": -89.206564, "name": "Pulaski", "rank": 2.16, "resolver": "place", "state": "IL"} -{"aliases": ["quincy", "quincy il", "quincy illinois"], "attrs": {"land_km2": 41.2, "pop": 38786, "type": "city"}, "id": "place:1762367", "key": "1762367", "kind": "place", "lat": 39.933675, "lon": -91.379304, "name": "Quincy", "rank": 4.59, "resolver": "place", "state": "IL"} +{"aliases": ["gem city", "quincy", "quincy il", "quincy illinois"], "attrs": {"land_km2": 41.2, "pop": 38786, "type": "city"}, "id": "place:1762367", "key": "1762367", "kind": "place", "lat": 39.933675, "lon": -91.379304, "name": "Quincy", "rank": 4.59, "resolver": "place", "state": "IL"} {"aliases": ["radom", "radom il", "radom illinois"], "attrs": {"land_km2": 2.7, "pop": 181, "type": "village"}, "id": "place:1762523", "key": "1762523", "kind": "place", "lat": 38.27984, "lon": -89.192113, "name": "Radom", "rank": 2.26, "resolver": "place", "state": "IL"} {"aliases": ["raleigh", "raleigh il", "raleigh illinois"], "attrs": {"land_km2": 5.1, "pop": 278, "type": "village"}, "id": "place:1762588", "key": "1762588", "kind": "place", "lat": 37.824449, "lon": -88.531202, "name": "Raleigh", "rank": 2.44, "resolver": "place", "state": "IL"} {"aliases": ["ramsey", "ramsey il", "ramsey illinois"], "attrs": {"land_km2": 2.9, "pop": 895, "type": "village"}, "id": "place:1762627", "key": "1762627", "kind": "place", "lat": 39.145659, "lon": -89.110269, "name": "Ramsey", "rank": 2.95, "resolver": "place", "state": "IL"} @@ -8000,7 +8000,7 @@ {"aliases": ["bluffton", "bluffton in", "bluffton indiana"], "attrs": {"land_km2": 22.1, "pop": 10556, "type": "city"}, "id": "place:1806220", "key": "1806220", "kind": "place", "lat": 40.743479, "lon": -85.172111, "name": "Bluffton", "rank": 4.02, "resolver": "place", "state": "IN"} {"aliases": ["boggstown", "boggstown in", "boggstown indiana"], "attrs": {"land_km2": 0.9, "pop": 0, "type": "cdp"}, "id": "place:1806292", "key": "1806292", "kind": "place", "lat": 39.562914, "lon": -85.912138, "name": "Boggstown", "rank": 0.0, "resolver": "place", "state": "IN"} {"aliases": ["boone grove", "boone grove in", "boone grove indiana"], "attrs": {"land_km2": 0.7, "pop": 0, "type": "cdp"}, "id": "place:1806598", "key": "1806598", "kind": "place", "lat": 41.35461, "lon": -87.134127, "name": "Boone Grove", "rank": 0.0, "resolver": "place", "state": "IN"} -{"aliases": ["boonville", "boonville in", "boonville indiana"], "attrs": {"land_km2": 14.8, "pop": 6687, "type": "city"}, "id": "place:1806616", "key": "1806616", "kind": "place", "lat": 38.04661, "lon": -87.291027, "name": "Boonville", "rank": 3.83, "resolver": "place", "state": "IN"} +{"aliases": ["boontucky", "boonville", "boonville in", "boonville indiana"], "attrs": {"land_km2": 14.8, "pop": 6687, "type": "city"}, "id": "place:1806616", "key": "1806616", "kind": "place", "lat": 38.04661, "lon": -87.291027, "name": "Boonville", "rank": 3.83, "resolver": "place", "state": "IN"} {"aliases": ["borden", "borden in", "borden indiana"], "attrs": {"land_km2": 3.6, "pop": 816, "type": "town"}, "id": "place:1806634", "key": "1806634", "kind": "place", "lat": 38.47198, "lon": -85.948165, "name": "Borden", "rank": 2.91, "resolver": "place", "state": "IN"} {"aliases": ["boston", "boston in", "boston indiana"], "attrs": {"land_km2": 0.6, "pop": 152, "type": "town"}, "id": "place:1806652", "key": "1806652", "kind": "place", "lat": 39.741167, "lon": -84.851792, "name": "Boston", "rank": 2.18, "resolver": "place", "state": "IN"} {"aliases": ["boswell", "boswell in", "boswell indiana"], "attrs": {"land_km2": 2.3, "pop": 811, "type": "town"}, "id": "place:1806706", "key": "1806706", "kind": "place", "lat": 40.517189, "lon": -87.375343, "name": "Boswell", "rank": 2.91, "resolver": "place", "state": "IN"} @@ -8204,7 +8204,7 @@ {"aliases": ["forest", "forest in", "forest indiana"], "attrs": {"land_km2": 0.6, "pop": 0, "type": "cdp"}, "id": "place:1823926", "key": "1823926", "kind": "place", "lat": 40.373861, "lon": -86.33227, "name": "Forest", "rank": 0.0, "resolver": "place", "state": "IN"} {"aliases": ["fort branch", "fort branch in", "fort branch indiana"], "attrs": {"land_km2": 2.8, "pop": 2935, "type": "town"}, "id": "place:1824250", "key": "1824250", "kind": "place", "lat": 38.245827, "lon": -87.573215, "name": "Fort Branch", "rank": 3.47, "resolver": "place", "state": "IN"} {"aliases": ["fortville", "fortville in", "fortville indiana"], "attrs": {"land_km2": 9.4, "pop": 4927, "type": "town"}, "id": "place:1824286", "key": "1824286", "kind": "place", "lat": 39.924807, "lon": -85.846348, "name": "Fortville", "rank": 3.69, "resolver": "place", "state": "IN"} -{"aliases": ["fort wayne", "fort wayne in", "fort wayne indiana"], "attrs": {"land_km2": 289.5, "pop": 273203, "type": "city"}, "id": "place:1825000", "key": "1825000", "kind": "place", "lat": 41.088193, "lon": -85.142791, "name": "Fort Wayne", "rank": 5.44, "resolver": "place", "state": "IN"} +{"aliases": ["city of churches", "fort wayne", "fort wayne in", "fort wayne indiana"], "attrs": {"land_km2": 289.5, "pop": 273203, "type": "city"}, "id": "place:1825000", "key": "1825000", "kind": "place", "lat": 41.088193, "lon": -85.142791, "name": "Fort Wayne", "rank": 5.44, "resolver": "place", "state": "IN"} {"aliases": ["foster", "foster in", "foster indiana"], "attrs": {"land_km2": 3.3, "pop": 0, "type": "cdp"}, "id": "place:1825054", "key": "1825054", "kind": "place", "lat": 40.14157, "lon": -87.468653, "name": "Foster", "rank": 0.0, "resolver": "place", "state": "IN"} {"aliases": ["fountain", "fountain city", "fountain city in", "fountain city indiana", "fountain in", "fountain indiana"], "attrs": {"land_km2": 0.7, "pop": 698, "type": "town"}, "id": "place:1825090", "key": "1825090", "kind": "place", "lat": 39.954741, "lon": -84.919137, "name": "Fountain City", "rank": 2.84, "resolver": "place", "state": "IN"} {"aliases": ["fountaintown", "fountaintown in", "fountaintown indiana"], "attrs": {"land_km2": 2.3, "pop": 0, "type": "cdp"}, "id": "place:1825162", "key": "1825162", "kind": "place", "lat": 39.69404, "lon": -85.782009, "name": "Fountaintown", "rank": 0.0, "resolver": "place", "state": "IN"} @@ -8321,7 +8321,7 @@ {"aliases": ["hymera", "hymera in", "hymera indiana"], "attrs": {"land_km2": 1.8, "pop": 661, "type": "town"}, "id": "place:1835500", "key": "1835500", "kind": "place", "lat": 39.185877, "lon": -87.298928, "name": "Hymera", "rank": 2.82, "resolver": "place", "state": "IN"} {"aliases": ["idaville", "idaville in", "idaville indiana"], "attrs": {"land_km2": 15.1, "pop": 0, "type": "cdp"}, "id": "place:1835536", "key": "1835536", "kind": "place", "lat": 40.758765, "lon": -86.650563, "name": "Idaville", "rank": 0.0, "resolver": "place", "state": "IN"} {"aliases": ["independence", "independence in", "independence indiana"], "attrs": {"land_km2": 0.5, "pop": 0, "type": "cdp"}, "id": "place:1835626", "key": "1835626", "kind": "place", "lat": 40.337949, "lon": -87.171645, "name": "Independence", "rank": 0.0, "resolver": "place", "state": "IN"} -{"aliases": ["indianapolis", "indianapolis in", "indianapolis indiana"], "attrs": {"land_km2": 935.0, "pop": 891484, "type": "city (balance)"}, "id": "place:1836003", "key": "1836003", "kind": "place", "lat": 39.776664, "lon": -86.145935, "name": "Indianapolis", "rank": 5.95, "resolver": "place", "state": "IN"} +{"aliases": ["indianapolis", "indianapolis in", "indianapolis indiana", "indy"], "attrs": {"land_km2": 935.0, "pop": 891484, "type": "city (balance)"}, "id": "place:1836003", "key": "1836003", "kind": "place", "lat": 39.776664, "lon": -86.145935, "name": "Indianapolis", "rank": 5.95, "resolver": "place", "state": "IN"} {"aliases": ["indian village", "indian village in", "indian village indiana"], "attrs": {"land_km2": 0.2, "pop": 115, "type": "town"}, "id": "place:1836306", "key": "1836306", "kind": "place", "lat": 41.714271, "lon": -86.23224, "name": "Indian Village", "rank": 2.06, "resolver": "place", "state": "IN"} {"aliases": ["ingalls", "ingalls in", "ingalls indiana"], "attrs": {"land_km2": 7.8, "pop": 2627, "type": "town"}, "id": "place:1836342", "key": "1836342", "kind": "place", "lat": 39.95895, "lon": -85.805753, "name": "Ingalls", "rank": 3.42, "resolver": "place", "state": "IN"} {"aliases": ["ireland", "ireland in", "ireland indiana"], "attrs": {"land_km2": 0.7, "pop": 0, "type": "cdp"}, "id": "place:1836468", "key": "1836468", "kind": "place", "lat": 38.414399, "lon": -86.998844, "name": "Ireland", "rank": 0.0, "resolver": "place", "state": "IN"} @@ -10863,7 +10863,7 @@ {"aliases": ["fulton", "fulton kentucky", "fulton ky"], "attrs": {"land_km2": 7.5, "pop": 2268, "type": "city"}, "id": "place:2129566", "key": "2129566", "kind": "place", "lat": 36.514477, "lon": -88.882391, "name": "Fulton", "rank": 3.36, "resolver": "place", "state": "KY"} {"aliases": ["gamaliel", "gamaliel kentucky", "gamaliel ky"], "attrs": {"land_km2": 2.0, "pop": 467, "type": "city"}, "id": "place:2129872", "key": "2129872", "kind": "place", "lat": 36.640297, "lon": -85.793521, "name": "Gamaliel", "rank": 2.67, "resolver": "place", "state": "KY"} {"aliases": ["garrison", "garrison kentucky", "garrison ky"], "attrs": {"land_km2": 7.0, "pop": 0, "type": "cdp"}, "id": "place:2130232", "key": "2130232", "kind": "place", "lat": 38.606721, "lon": -83.181911, "name": "Garrison", "rank": 0.0, "resolver": "place", "state": "KY"} -{"aliases": ["georgetown", "georgetown kentucky", "georgetown ky"], "attrs": {"land_km2": 45.8, "pop": 40518, "type": "city"}, "id": "place:2130700", "key": "2130700", "kind": "place", "lat": 38.260181, "lon": -84.534921, "name": "Georgetown", "rank": 4.61, "resolver": "place", "state": "KY"} +{"aliases": ["g town", "georgetown", "georgetown kentucky", "georgetown ky"], "attrs": {"land_km2": 45.8, "pop": 40518, "type": "city"}, "id": "place:2130700", "key": "2130700", "kind": "place", "lat": 38.260181, "lon": -84.534921, "name": "Georgetown", "rank": 4.61, "resolver": "place", "state": "KY"} {"aliases": ["germantown", "germantown kentucky", "germantown ky"], "attrs": {"land_km2": 0.3, "pop": 143, "type": "city"}, "id": "place:2130718", "key": "2130718", "kind": "place", "lat": 38.655922, "lon": -83.964156, "name": "Germantown", "rank": 2.16, "resolver": "place", "state": "KY"} {"aliases": ["ghent", "ghent kentucky", "ghent ky"], "attrs": {"land_km2": 1.8, "pop": 369, "type": "city"}, "id": "place:2130808", "key": "2130808", "kind": "place", "lat": 38.732485, "lon": -85.063734, "name": "Ghent", "rank": 2.57, "resolver": "place", "state": "KY"} {"aliases": ["gilbertsville", "gilbertsville kentucky", "gilbertsville ky"], "attrs": {"land_km2": 2.6, "pop": 0, "type": "cdp"}, "id": "place:2130880", "key": "2130880", "kind": "place", "lat": 37.024782, "lon": -88.310973, "name": "Gilbertsville", "rank": 0.0, "resolver": "place", "state": "KY"} @@ -11545,7 +11545,7 @@ {"aliases": ["newellton", "newellton la", "newellton louisiana"], "attrs": {"land_km2": 2.0, "pop": 828, "type": "town"}, "id": "place:2253930", "key": "2253930", "kind": "place", "lat": 32.072601, "lon": -91.239269, "name": "Newellton", "rank": 2.92, "resolver": "place", "state": "LA"} {"aliases": ["new iberia", "new iberia la", "new iberia louisiana"], "attrs": {"land_km2": 28.9, "pop": 26849, "type": "city"}, "id": "place:2254035", "key": "2254035", "kind": "place", "lat": 30.005493, "lon": -91.820307, "name": "New Iberia", "rank": 4.43, "resolver": "place", "state": "LA"} {"aliases": ["new llano", "new llano la", "new llano louisiana"], "attrs": {"land_km2": 6.9, "pop": 2064, "type": "town"}, "id": "place:2254175", "key": "2254175", "kind": "place", "lat": 31.105733, "lon": -93.269609, "name": "New Llano", "rank": 3.31, "resolver": "place", "state": "LA"} -{"aliases": ["new orleans", "new orleans la", "new orleans louisiana"], "attrs": {"land_km2": 439.0, "pop": 362701, "type": "city"}, "id": "place:2255000", "key": "2255000", "kind": "place", "lat": 30.05342, "lon": -89.934502, "name": "New Orleans", "rank": 5.56, "resolver": "place", "state": "LA"} +{"aliases": ["big easy", "new orleans", "new orleans la", "new orleans louisiana", "nola"], "attrs": {"land_km2": 439.0, "pop": 362701, "type": "city"}, "id": "place:2255000", "key": "2255000", "kind": "place", "lat": 30.05342, "lon": -89.934502, "name": "New Orleans", "rank": 5.56, "resolver": "place", "state": "LA"} {"aliases": ["new orleans station", "new orleans station la", "new orleans station louisiana"], "attrs": {"land_km2": 13.5, "pop": 0, "type": "cdp"}, "id": "place:2255074", "key": "2255074", "kind": "place", "lat": 29.827388, "lon": -90.024987, "name": "New Orleans Station", "rank": 0.0, "resolver": "place", "state": "LA"} {"aliases": ["new roads", "new roads la", "new roads louisiana"], "attrs": {"land_km2": 11.9, "pop": 4295, "type": "city"}, "id": "place:2255105", "key": "2255105", "kind": "place", "lat": 30.695921, "lon": -91.453794, "name": "New Roads", "rank": 3.63, "resolver": "place", "state": "LA"} {"aliases": ["new sarpy", "new sarpy la", "new sarpy louisiana"], "attrs": {"land_km2": 3.5, "pop": 0, "type": "cdp"}, "id": "place:2255140", "key": "2255140", "kind": "place", "lat": 29.979168, "lon": -90.383494, "name": "New Sarpy", "rank": 0.0, "resolver": "place", "state": "LA"} @@ -11867,7 +11867,7 @@ {"aliases": ["yarmouth", "yarmouth maine", "yarmouth me"], "attrs": {"land_km2": 13.6, "pop": 0, "type": "cdp"}, "id": "place:2387810", "key": "2387810", "kind": "place", "lat": 43.803815, "lon": -70.18858, "name": "Yarmouth", "rank": 0.0, "resolver": "place", "state": "ME"} {"aliases": ["york harbor", "york harbor maine", "york harbor me"], "attrs": {"land_km2": 8.3, "pop": 0, "type": "cdp"}, "id": "place:2388160", "key": "2388160", "kind": "place", "lat": 43.143022, "lon": -70.647489, "name": "York Harbor", "rank": 0.0, "resolver": "place", "state": "ME"} {"aliases": ["aberdeen", "aberdeen maryland", "aberdeen md"], "attrs": {"land_km2": 18.0, "pop": 18451, "type": "city"}, "id": "place:2400125", "key": "2400125", "kind": "place", "lat": 39.516403, "lon": -76.174518, "name": "Aberdeen", "rank": 4.27, "resolver": "place", "state": "MD"} -{"aliases": ["aberdeen proving ground", "aberdeen proving ground maryland", "aberdeen proving ground md"], "attrs": {"land_km2": 28.8, "pop": 0, "type": "cdp"}, "id": "place:2400175", "key": "2400175", "kind": "place", "lat": 39.454232, "lon": -76.133542, "name": "Aberdeen Proving Ground", "rank": 0.0, "resolver": "place", "state": "MD"} +{"aliases": ["aberdeen proving ground", "aberdeen proving ground maryland", "aberdeen proving ground md", "apg"], "attrs": {"land_km2": 28.8, "pop": 0, "type": "cdp"}, "id": "place:2400175", "key": "2400175", "kind": "place", "lat": 39.454232, "lon": -76.133542, "name": "Aberdeen Proving Ground", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["abingdon", "abingdon maryland", "abingdon md"], "attrs": {"land_km2": 7.2, "pop": 0, "type": "cdp"}, "id": "place:2400200", "key": "2400200", "kind": "place", "lat": 39.463472, "lon": -76.27314, "name": "Abingdon", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["accident", "accident maryland", "accident md"], "attrs": {"land_km2": 1.3, "pop": 332, "type": "town"}, "id": "place:2400225", "key": "2400225", "kind": "place", "lat": 39.625688, "lon": -79.319958, "name": "Accident", "rank": 2.52, "resolver": "place", "state": "MD"} {"aliases": ["accokeek", "accokeek maryland", "accokeek md"], "attrs": {"land_km2": 71.0, "pop": 0, "type": "cdp"}, "id": "place:2400250", "key": "2400250", "kind": "place", "lat": 38.676533, "lon": -77.000518, "name": "Accokeek", "rank": 0.0, "resolver": "place", "state": "MD"} @@ -11875,8 +11875,8 @@ {"aliases": ["adelphi", "adelphi maryland", "adelphi md"], "attrs": {"land_km2": 7.0, "pop": 0, "type": "cdp"}, "id": "place:2400400", "key": "2400400", "kind": "place", "lat": 38.997067, "lon": -76.966783, "name": "Adelphi", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["algonquin", "algonquin maryland", "algonquin md"], "attrs": {"land_km2": 1.9, "pop": 0, "type": "cdp"}, "id": "place:2400712", "key": "2400712", "kind": "place", "lat": 38.589853, "lon": -76.091558, "name": "Algonquin", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["allen", "allen maryland", "allen md"], "attrs": {"land_km2": 1.1, "pop": 0, "type": "cdp"}, "id": "place:2400800", "key": "2400800", "kind": "place", "lat": 38.288387, "lon": -75.693051, "name": "Allen", "rank": 0.0, "resolver": "place", "state": "MD"} -{"aliases": ["andrews afb", "andrews afb maryland", "andrews afb md"], "attrs": {"land_km2": 18.0, "pop": 0, "type": "cdp"}, "id": "place:2401450", "key": "2401450", "kind": "place", "lat": 38.809803, "lon": -76.869158, "name": "Andrews AFB", "rank": 0.0, "resolver": "place", "state": "MD"} -{"aliases": ["annapolis", "annapolis maryland", "annapolis md"], "attrs": {"land_km2": 18.7, "pop": 40689, "type": "city"}, "id": "place:2401600", "key": "2401600", "kind": "place", "lat": 38.972152, "lon": -76.505332, "name": "Annapolis", "rank": 4.61, "resolver": "place", "state": "MD"} +{"aliases": ["andrews afb", "andrews afb maryland", "andrews afb md", "jb andrews", "jba"], "attrs": {"land_km2": 18.0, "pop": 0, "type": "cdp"}, "id": "place:2401450", "key": "2401450", "kind": "place", "lat": 38.809803, "lon": -76.869158, "name": "Andrews AFB", "rank": 0.0, "resolver": "place", "state": "MD"} +{"aliases": ["annapolis", "annapolis maryland", "annapolis md", "naptown"], "attrs": {"land_km2": 18.7, "pop": 40689, "type": "city"}, "id": "place:2401600", "key": "2401600", "kind": "place", "lat": 38.972152, "lon": -76.505332, "name": "Annapolis", "rank": 4.61, "resolver": "place", "state": "MD"} {"aliases": ["annapolis neck", "annapolis neck maryland", "annapolis neck md"], "attrs": {"land_km2": 18.0, "pop": 0, "type": "cdp"}, "id": "place:2401635", "key": "2401635", "kind": "place", "lat": 38.936562, "lon": -76.498084, "name": "Annapolis Neck", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["antietam", "antietam maryland", "antietam md"], "attrs": {"land_km2": 0.4, "pop": 0, "type": "cdp"}, "id": "place:2401750", "key": "2401750", "kind": "place", "lat": 39.415082, "lon": -77.736484, "name": "Antietam", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["aquasco", "aquasco maryland", "aquasco md"], "attrs": {"land_km2": 51.2, "pop": 0, "type": "cdp"}, "id": "place:2401925", "key": "2401925", "kind": "place", "lat": 38.591534, "lon": -76.697125, "name": "Aquasco", "rank": 0.0, "resolver": "place", "state": "MD"} @@ -11889,7 +11889,7 @@ {"aliases": ["bagtown", "bagtown maryland", "bagtown md"], "attrs": {"land_km2": 1.4, "pop": 0, "type": "cdp"}, "id": "place:2403514", "key": "2403514", "kind": "place", "lat": 39.582982, "lon": -77.613885, "name": "Bagtown", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["bakersville", "bakersville maryland", "bakersville md"], "attrs": {"land_km2": 0.1, "pop": 0, "type": "cdp"}, "id": "place:2403575", "key": "2403575", "kind": "place", "lat": 39.514829, "lon": -77.757048, "name": "Bakersville", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["ballenger creek", "ballenger creek maryland", "ballenger creek md"], "attrs": {"land_km2": 28.0, "pop": 0, "type": "cdp"}, "id": "place:2403800", "key": "2403800", "kind": "place", "lat": 39.381131, "lon": -77.420179, "name": "Ballenger Creek", "rank": 0.0, "resolver": "place", "state": "MD"} -{"aliases": ["baltimore", "baltimore maryland", "baltimore md"], "attrs": {"land_km2": 209.6, "pop": 568271, "type": "city"}, "id": "place:2404000", "key": "2404000", "kind": "place", "lat": 39.300032, "lon": -76.610476, "name": "Baltimore", "rank": 5.75, "resolver": "place", "state": "MD"} +{"aliases": ["baltimore", "baltimore maryland", "baltimore md", "bmore", "charm city"], "attrs": {"land_km2": 209.6, "pop": 568271, "type": "city"}, "id": "place:2404000", "key": "2404000", "kind": "place", "lat": 39.300032, "lon": -76.610476, "name": "Baltimore", "rank": 5.75, "resolver": "place", "state": "MD"} {"aliases": ["baltimore highlands", "baltimore highlands maryland", "baltimore highlands md"], "attrs": {"land_km2": 4.5, "pop": 0, "type": "cdp"}, "id": "place:2404050", "key": "2404050", "kind": "place", "lat": 39.235932, "lon": -76.637402, "name": "Baltimore Highlands", "rank": 0.0, "resolver": "place", "state": "MD"} {"aliases": ["barclay", "barclay maryland", "barclay md"], "attrs": {"land_km2": 1.0, "pop": 181, "type": "town"}, "id": "place:2404250", "key": "2404250", "kind": "place", "lat": 39.146454, "lon": -75.864433, "name": "Barclay", "rank": 2.26, "resolver": "place", "state": "MD"} {"aliases": ["barnesville", "barnesville maryland", "barnesville md"], "attrs": {"land_km2": 1.3, "pop": 145, "type": "town"}, "id": "place:2404475", "key": "2404475", "kind": "place", "lat": 39.22392, "lon": -77.376106, "name": "Barnesville", "rank": 2.16, "resolver": "place", "state": "MD"} @@ -12422,7 +12422,7 @@ {"aliases": ["beverly", "beverly ma", "beverly massachusetts"], "attrs": {"land_km2": 39.1, "pop": 43223, "type": "city"}, "id": "place:2505595", "key": "2505595", "kind": "place", "lat": 42.556659, "lon": -70.84496, "name": "Beverly", "rank": 4.64, "resolver": "place", "state": "MA"} {"aliases": ["blandford", "blandford ma", "blandford massachusetts"], "attrs": {"land_km2": 3.0, "pop": 0, "type": "cdp"}, "id": "place:2506050", "key": "2506050", "kind": "place", "lat": 42.175006, "lon": -72.923862, "name": "Blandford", "rank": 0.0, "resolver": "place", "state": "MA"} {"aliases": ["bliss corner", "bliss corner ma", "bliss corner massachusetts"], "attrs": {"land_km2": 5.2, "pop": 0, "type": "cdp"}, "id": "place:2506170", "key": "2506170", "kind": "place", "lat": 41.605131, "lon": -70.942369, "name": "Bliss Corner", "rank": 0.0, "resolver": "place", "state": "MA"} -{"aliases": ["boston", "boston ma", "boston massachusetts"], "attrs": {"land_km2": 125.2, "pop": 673458, "type": "city"}, "id": "place:2507000", "key": "2507000", "kind": "place", "lat": 42.338551, "lon": -71.018253, "name": "Boston", "rank": 5.83, "resolver": "place", "state": "MA"} +{"aliases": ["beantown", "boston", "boston ma", "boston massachusetts", "cradle of liberty"], "attrs": {"land_km2": 125.2, "pop": 673458, "type": "city"}, "id": "place:2507000", "key": "2507000", "kind": "place", "lat": 42.338551, "lon": -71.018253, "name": "Boston", "rank": 5.83, "resolver": "place", "state": "MA"} {"aliases": ["bourne", "bourne ma", "bourne massachusetts"], "attrs": {"land_km2": 4.4, "pop": 0, "type": "cdp"}, "id": "place:2507140", "key": "2507140", "kind": "place", "lat": 41.732015, "lon": -70.615767, "name": "Bourne", "rank": 0.0, "resolver": "place", "state": "MA"} {"aliases": ["boxford", "boxford ma", "boxford massachusetts"], "attrs": {"land_km2": 13.9, "pop": 0, "type": "cdp"}, "id": "place:2507385", "key": "2507385", "kind": "place", "lat": 42.663712, "lon": -70.984216, "name": "Boxford", "rank": 0.0, "resolver": "place", "state": "MA"} {"aliases": ["braintree town", "braintree town ma", "braintree town massachusetts"], "attrs": {"land_km2": 35.6, "pop": 39134, "type": "city"}, "id": "place:2507740", "key": "2507740", "kind": "place", "lat": 42.206188, "lon": -71.00232, "name": "Braintree Town", "rank": 4.59, "resolver": "place", "state": "MA"} @@ -12830,7 +12830,7 @@ {"aliases": ["deerfield", "deerfield mi", "deerfield michigan"], "attrs": {"land_km2": 2.5, "pop": 886, "type": "village"}, "id": "place:2621180", "key": "2621180", "kind": "place", "lat": 41.890188, "lon": -83.778855, "name": "Deerfield", "rank": 2.95, "resolver": "place", "state": "MI"} {"aliases": ["delton", "delton mi", "delton michigan"], "attrs": {"land_km2": 5.7, "pop": 0, "type": "cdp"}, "id": "place:2621560", "key": "2621560", "kind": "place", "lat": 42.494554, "lon": -85.413167, "name": "Delton", "rank": 0.0, "resolver": "place", "state": "MI"} {"aliases": ["de tour village", "de tour village mi", "de tour village michigan"], "attrs": {"land_km2": 9.1, "pop": 263, "type": "village"}, "id": "place:2621780", "key": "2621780", "kind": "place", "lat": 45.993234, "lon": -83.916354, "name": "De Tour Village", "rank": 2.42, "resolver": "place", "state": "MI"} -{"aliases": ["detroit", "detroit mi", "detroit michigan"], "attrs": {"land_km2": 359.3, "pop": 645705, "type": "city"}, "id": "place:2622000", "key": "2622000", "kind": "place", "lat": 42.383037, "lon": -83.102237, "name": "Detroit", "rank": 5.81, "resolver": "place", "state": "MI"} +{"aliases": ["detroit", "detroit mi", "detroit michigan", "motor city", "motown"], "attrs": {"land_km2": 359.3, "pop": 645705, "type": "city"}, "id": "place:2622000", "key": "2622000", "kind": "place", "lat": 42.383037, "lon": -83.102237, "name": "Detroit", "rank": 5.81, "resolver": "place", "state": "MI"} {"aliases": ["detroit beach", "detroit beach mi", "detroit beach michigan"], "attrs": {"land_km2": 1.6, "pop": 0, "type": "cdp"}, "id": "place:2622040", "key": "2622040", "kind": "place", "lat": 41.931078, "lon": -83.328277, "name": "Detroit Beach", "rank": 0.0, "resolver": "place", "state": "MI"} {"aliases": ["dewitt", "dewitt mi", "dewitt michigan"], "attrs": {"land_km2": 7.3, "pop": 4780, "type": "city"}, "id": "place:2622120", "key": "2622120", "kind": "place", "lat": 42.838828, "lon": -84.583837, "name": "DeWitt", "rank": 3.68, "resolver": "place", "state": "MI"} {"aliases": ["dexter", "dexter mi", "dexter michigan"], "attrs": {"land_km2": 5.0, "pop": 4528, "type": "city"}, "id": "place:2622160", "key": "2622160", "kind": "place", "lat": 42.332256, "lon": -83.881027, "name": "Dexter", "rank": 3.66, "resolver": "place", "state": "MI"} @@ -12925,7 +12925,7 @@ {"aliases": ["grand haven", "grand haven mi", "grand haven michigan"], "attrs": {"land_km2": 14.8, "pop": 11045, "type": "city"}, "id": "place:2633340", "key": "2633340", "kind": "place", "lat": 43.054969, "lon": -86.219349, "name": "Grand Haven", "rank": 4.04, "resolver": "place", "state": "MI"} {"aliases": ["grand ledge", "grand ledge mi", "grand ledge michigan"], "attrs": {"land_km2": 10.0, "pop": 7838, "type": "city"}, "id": "place:2633420", "key": "2633420", "kind": "place", "lat": 42.752927, "lon": -84.744901, "name": "Grand Ledge", "rank": 3.89, "resolver": "place", "state": "MI"} {"aliases": ["grand marais", "grand marais mi", "grand marais michigan"], "attrs": {"land_km2": 4.9, "pop": 0, "type": "cdp"}, "id": "place:2633440", "key": "2633440", "kind": "place", "lat": 46.675701, "lon": -85.96643, "name": "Grand Marais", "rank": 0.0, "resolver": "place", "state": "MI"} -{"aliases": ["grand rapids", "grand rapids mi", "grand rapids michigan"], "attrs": {"land_km2": 116.0, "pop": 200117, "type": "city"}, "id": "place:2634000", "key": "2634000", "kind": "place", "lat": 42.961156, "lon": -85.65557, "name": "Grand Rapids", "rank": 5.3, "resolver": "place", "state": "MI"} +{"aliases": ["furniture city", "grand rapids", "grand rapids mi", "grand rapids michigan"], "attrs": {"land_km2": 116.0, "pop": 200117, "type": "city"}, "id": "place:2634000", "key": "2634000", "kind": "place", "lat": 42.961156, "lon": -85.65557, "name": "Grand Rapids", "rank": 5.3, "resolver": "place", "state": "MI"} {"aliases": ["grandville", "grandville mi", "grandville michigan"], "attrs": {"land_km2": 18.8, "pop": 17094, "type": "city"}, "id": "place:2634160", "key": "2634160", "kind": "place", "lat": 42.900514, "lon": -85.752086, "name": "Grandville", "rank": 4.23, "resolver": "place", "state": "MI"} {"aliases": ["grant", "grant mi", "grant michigan"], "attrs": {"land_km2": 1.7, "pop": 987, "type": "city"}, "id": "place:2634360", "key": "2634360", "kind": "place", "lat": 43.333986, "lon": -85.809879, "name": "Grant", "rank": 2.99, "resolver": "place", "state": "MI"} {"aliases": ["grass lake", "grass lake mi", "grass lake michigan"], "attrs": {"land_km2": 2.4, "pop": 1101, "type": "village"}, "id": "place:2634480", "key": "2634480", "kind": "place", "lat": 42.250798, "lon": -84.206176, "name": "Grass Lake", "rank": 3.04, "resolver": "place", "state": "MI"} @@ -13951,7 +13951,7 @@ {"aliases": ["millville", "millville minnesota", "millville mn"], "attrs": {"land_km2": 0.4, "pop": 144, "type": "city"}, "id": "place:2742290", "key": "2742290", "kind": "place", "lat": 44.244329, "lon": -92.296734, "name": "Millville", "rank": 2.16, "resolver": "place", "state": "MN"} {"aliases": ["milroy", "milroy minnesota", "milroy mn"], "attrs": {"land_km2": 0.7, "pop": 254, "type": "city"}, "id": "place:2742362", "key": "2742362", "kind": "place", "lat": 44.41775, "lon": -95.553595, "name": "Milroy", "rank": 2.4, "resolver": "place", "state": "MN"} {"aliases": ["miltona", "miltona minnesota", "miltona mn"], "attrs": {"land_km2": 1.7, "pop": 442, "type": "city"}, "id": "place:2742398", "key": "2742398", "kind": "place", "lat": 46.046258, "lon": -95.293197, "name": "Miltona", "rank": 2.65, "resolver": "place", "state": "MN"} -{"aliases": ["minneapolis", "minneapolis minnesota", "minneapolis mn"], "attrs": {"land_km2": 139.9, "pop": 428579, "type": "city"}, "id": "place:2743000", "key": "2743000", "kind": "place", "lat": 44.963324, "lon": -93.26832, "name": "Minneapolis", "rank": 5.63, "resolver": "place", "state": "MN"} +{"aliases": ["minneapolis", "minneapolis minnesota", "minneapolis mn", "mpls"], "attrs": {"land_km2": 139.9, "pop": 428579, "type": "city"}, "id": "place:2743000", "key": "2743000", "kind": "place", "lat": 44.963324, "lon": -93.26832, "name": "Minneapolis", "rank": 5.63, "resolver": "place", "state": "MN"} {"aliases": ["minneiska", "minneiska minnesota", "minneiska mn"], "attrs": {"land_km2": 1.4, "pop": 97, "type": "city"}, "id": "place:2743036", "key": "2743036", "kind": "place", "lat": 44.197331, "lon": -91.880651, "name": "Minneiska", "rank": 1.99, "resolver": "place", "state": "MN"} {"aliases": ["minneota", "minneota minnesota", "minneota mn"], "attrs": {"land_km2": 3.8, "pop": 1355, "type": "city"}, "id": "place:2743126", "key": "2743126", "kind": "place", "lat": 44.562526, "lon": -95.98272, "name": "Minneota", "rank": 3.13, "resolver": "place", "state": "MN"} {"aliases": ["minnesota city", "minnesota city minnesota", "minnesota city mn"], "attrs": {"land_km2": 0.6, "pop": 183, "type": "city"}, "id": "place:2743144", "key": "2743144", "kind": "place", "lat": 44.092296, "lon": -91.749995, "name": "Minnesota City", "rank": 2.26, "resolver": "place", "state": "MN"} @@ -14094,7 +14094,7 @@ {"aliases": ["riverland", "riverland minnesota", "riverland mn"], "attrs": {"land_km2": 0.6, "pop": 0, "type": "cdp"}, "id": "place:2754596", "key": "2754596", "kind": "place", "lat": 47.322899, "lon": -95.951066, "name": "Riverland", "rank": 0.0, "resolver": "place", "state": "MN"} {"aliases": ["riverton", "riverton minnesota", "riverton mn"], "attrs": {"land_km2": 2.1, "pop": 115, "type": "city"}, "id": "place:2754736", "key": "2754736", "kind": "place", "lat": 46.457035, "lon": -94.054461, "name": "Riverton", "rank": 2.06, "resolver": "place", "state": "MN"} {"aliases": ["robbinsdale", "robbinsdale minnesota", "robbinsdale mn"], "attrs": {"land_km2": 7.2, "pop": 13993, "type": "city"}, "id": "place:2754808", "key": "2754808", "kind": "place", "lat": 45.024534, "lon": -93.330644, "name": "Robbinsdale", "rank": 4.15, "resolver": "place", "state": "MN"} -{"aliases": ["rochester", "rochester minnesota", "rochester mn"], "attrs": {"land_km2": 146.8, "pop": 123624, "type": "city"}, "id": "place:2754880", "key": "2754880", "kind": "place", "lat": 44.014299, "lon": -92.478592, "name": "Rochester", "rank": 5.09, "resolver": "place", "state": "MN"} +{"aliases": ["med city", "rochester", "rochester minnesota", "rochester mn"], "attrs": {"land_km2": 146.8, "pop": 123624, "type": "city"}, "id": "place:2754880", "key": "2754880", "kind": "place", "lat": 44.014299, "lon": -92.478592, "name": "Rochester", "rank": 5.09, "resolver": "place", "state": "MN"} {"aliases": ["rock creek", "rock creek minnesota", "rock creek mn"], "attrs": {"land_km2": 111.4, "pop": 1778, "type": "city"}, "id": "place:2754934", "key": "2754934", "kind": "place", "lat": 45.764654, "lon": -92.903506, "name": "Rock Creek", "rank": 3.25, "resolver": "place", "state": "MN"} {"aliases": ["rockford", "rockford minnesota", "rockford mn"], "attrs": {"land_km2": 6.5, "pop": 4796, "type": "city"}, "id": "place:2755006", "key": "2755006", "kind": "place", "lat": 45.090841, "lon": -93.745218, "name": "Rockford", "rank": 3.68, "resolver": "place", "state": "MN"} {"aliases": ["rockville", "rockville minnesota", "rockville mn"], "attrs": {"land_km2": 74.3, "pop": 2471, "type": "city"}, "id": "place:2755078", "key": "2755078", "kind": "place", "lat": 45.466049, "lon": -94.318918, "name": "Rockville", "rank": 3.39, "resolver": "place", "state": "MN"} @@ -14672,7 +14672,7 @@ {"aliases": ["soso", "soso mississippi", "soso ms"], "attrs": {"land_km2": 5.3, "pop": 395, "type": "town"}, "id": "place:2869160", "key": "2869160", "kind": "place", "lat": 31.75028, "lon": -89.276873, "name": "Soso", "rank": 2.6, "resolver": "place", "state": "MS"} {"aliases": ["southaven", "southaven mississippi", "southaven ms"], "attrs": {"land_km2": 107.0, "pop": 57493, "type": "city"}, "id": "place:2869280", "key": "2869280", "kind": "place", "lat": 34.951005, "lon": -89.977669, "name": "Southaven", "rank": 4.76, "resolver": "place", "state": "MS"} {"aliases": ["standing pine", "standing pine mississippi", "standing pine ms"], "attrs": {"land_km2": 9.0, "pop": 0, "type": "cdp"}, "id": "place:2870120", "key": "2870120", "kind": "place", "lat": 32.671027, "lon": -89.452088, "name": "Standing Pine", "rank": 0.0, "resolver": "place", "state": "MS"} -{"aliases": ["starkville", "starkville mississippi", "starkville ms"], "attrs": {"land_km2": 71.9, "pop": 25844, "type": "city"}, "id": "place:2870240", "key": "2870240", "kind": "place", "lat": 33.463261, "lon": -88.827796, "name": "Starkville", "rank": 4.41, "resolver": "place", "state": "MS"} +{"aliases": ["starkvegas", "starkville", "starkville mississippi", "starkville ms"], "attrs": {"land_km2": 71.9, "pop": 25844, "type": "city"}, "id": "place:2870240", "key": "2870240", "kind": "place", "lat": 33.463261, "lon": -88.827796, "name": "Starkville", "rank": 4.41, "resolver": "place", "state": "MS"} {"aliases": ["state line", "state line mississippi", "state line ms"], "attrs": {"land_km2": 11.0, "pop": 459, "type": "town"}, "id": "place:2870320", "key": "2870320", "kind": "place", "lat": 31.436095, "lon": -88.473477, "name": "State Line", "rank": 2.66, "resolver": "place", "state": "MS"} {"aliases": ["stewart", "stewart mississippi", "stewart ms"], "attrs": {"land_km2": 2.9, "pop": 0, "type": "cdp"}, "id": "place:2870640", "key": "2870640", "kind": "place", "lat": 33.451888, "lon": -89.437548, "name": "Stewart", "rank": 0.0, "resolver": "place", "state": "MS"} {"aliases": ["stoneville", "stoneville mississippi", "stoneville ms"], "attrs": {"land_km2": 4.9, "pop": 0, "type": "cdp"}, "id": "place:2870720", "key": "2870720", "kind": "place", "lat": 33.423804, "lon": -90.921629, "name": "Stoneville", "rank": 0.0, "resolver": "place", "state": "MS"} @@ -15247,7 +15247,7 @@ {"aliases": ["josephville", "josephville missouri", "josephville mo"], "attrs": {"land_km2": 7.4, "pop": 517, "type": "village"}, "id": "place:2937700", "key": "2937700", "kind": "place", "lat": 38.830523, "lon": -90.788292, "name": "Josephville", "rank": 2.71, "resolver": "place", "state": "MO"} {"aliases": ["junction", "junction city", "junction city missouri", "junction city mo", "junction missouri", "junction mo"], "attrs": {"land_km2": 0.9, "pop": 286, "type": "village"}, "id": "place:2937736", "key": "2937736", "kind": "place", "lat": 37.573962, "lon": -90.293249, "name": "Junction City", "rank": 2.46, "resolver": "place", "state": "MO"} {"aliases": ["kahoka", "kahoka missouri", "kahoka mo"], "attrs": {"land_km2": 4.1, "pop": 1973, "type": "city"}, "id": "place:2937790", "key": "2937790", "kind": "place", "lat": 40.42362, "lon": -91.718706, "name": "Kahoka", "rank": 3.3, "resolver": "place", "state": "MO"} -{"aliases": ["kansas city", "kansas city missouri", "kansas city mo"], "attrs": {"land_km2": 814.6, "pop": 516032, "type": "city"}, "id": "place:2938000", "key": "2938000", "kind": "place", "lat": 39.125155, "lon": -94.550313, "name": "Kansas City", "rank": 5.71, "resolver": "place", "state": "MO"} +{"aliases": ["kansas city", "kansas city missouri", "kansas city mo", "kc"], "attrs": {"land_km2": 814.6, "pop": 516032, "type": "city"}, "id": "place:2938000", "key": "2938000", "kind": "place", "lat": 39.125155, "lon": -94.550313, "name": "Kansas City", "rank": 5.71, "resolver": "place", "state": "MO"} {"aliases": ["kearney", "kearney missouri", "kearney mo"], "attrs": {"land_km2": 34.5, "pop": 11215, "type": "city"}, "id": "place:2938072", "key": "2938072", "kind": "place", "lat": 39.355111, "lon": -94.35858, "name": "Kearney", "rank": 4.05, "resolver": "place", "state": "MO"} {"aliases": ["kelso", "kelso missouri", "kelso mo"], "attrs": {"land_km2": 0.8, "pop": 553, "type": "village"}, "id": "place:2938216", "key": "2938216", "kind": "place", "lat": 37.192191, "lon": -89.550433, "name": "Kelso", "rank": 2.74, "resolver": "place", "state": "MO"} {"aliases": ["kennett", "kennett missouri", "kennett mo"], "attrs": {"land_km2": 18.1, "pop": 10137, "type": "city"}, "id": "place:2938306", "key": "2938306", "kind": "place", "lat": 36.240223, "lon": -90.047221, "name": "Kennett", "rank": 4.01, "resolver": "place", "state": "MO"} @@ -15619,7 +15619,7 @@ {"aliases": ["st james", "st james missouri", "st james mo"], "attrs": {"land_km2": 11.0, "pop": 3932, "type": "city"}, "id": "place:2964424", "key": "2964424", "kind": "place", "lat": 38.002356, "lon": -91.613372, "name": "St. James", "rank": 3.59, "resolver": "place", "state": "MO"} {"aliases": ["st john", "st john missouri", "st john mo"], "attrs": {"land_km2": 3.7, "pop": 6472, "type": "city"}, "id": "place:2964478", "key": "2964478", "kind": "place", "lat": 38.714758, "lon": -90.346265, "name": "St. John", "rank": 3.81, "resolver": "place", "state": "MO"} {"aliases": ["st joseph", "st joseph missouri", "st joseph mo"], "attrs": {"land_km2": 114.1, "pop": 71098, "type": "city"}, "id": "place:2964550", "key": "2964550", "kind": "place", "lat": 39.75946, "lon": -94.821143, "name": "St. Joseph", "rank": 4.85, "resolver": "place", "state": "MO"} -{"aliases": ["st louis", "st louis missouri", "st louis mo"], "attrs": {"land_km2": 159.9, "pop": 279695, "type": "city"}, "id": "place:2965000", "key": "2965000", "kind": "place", "lat": 38.635699, "lon": -90.244582, "name": "St. Louis", "rank": 5.45, "resolver": "place", "state": "MO"} +{"aliases": ["st louis", "st louis missouri", "st louis mo", "stl"], "attrs": {"land_km2": 159.9, "pop": 279695, "type": "city"}, "id": "place:2965000", "key": "2965000", "kind": "place", "lat": 38.635699, "lon": -90.244582, "name": "St. Louis", "rank": 5.45, "resolver": "place", "state": "MO"} {"aliases": ["st martins", "st martins missouri", "st martins mo"], "attrs": {"land_km2": 5.0, "pop": 1226, "type": "city"}, "id": "place:2965018", "key": "2965018", "kind": "place", "lat": 38.598322, "lon": -92.334372, "name": "St. Martins", "rank": 3.09, "resolver": "place", "state": "MO"} {"aliases": ["st mary", "st mary missouri", "st mary mo"], "attrs": {"land_km2": 1.6, "pop": 306, "type": "city"}, "id": "place:2965034", "key": "2965034", "kind": "place", "lat": 37.873824, "lon": -89.949659, "name": "St. Mary", "rank": 2.49, "resolver": "place", "state": "MO"} {"aliases": ["st paul", "st paul missouri", "st paul mo"], "attrs": {"land_km2": 18.7, "pop": 3725, "type": "city"}, "id": "place:2965108", "key": "2965108", "kind": "place", "lat": 38.848806, "lon": -90.740502, "name": "St. Paul", "rank": 3.57, "resolver": "place", "state": "MO"} @@ -16969,7 +16969,7 @@ {"aliases": ["kingston", "kingston nevada", "kingston nv"], "attrs": {"land_km2": 10.5, "pop": 0, "type": "cdp"}, "id": "place:3238100", "key": "3238100", "kind": "place", "lat": 39.210131, "lon": -117.067937, "name": "Kingston", "rank": 0.0, "resolver": "place", "state": "NV"} {"aliases": ["lakeridge", "lakeridge nevada", "lakeridge nv"], "attrs": {"land_km2": 3.8, "pop": 0, "type": "cdp"}, "id": "place:3239100", "key": "3239100", "kind": "place", "lat": 39.038183, "lon": -119.942792, "name": "Lakeridge", "rank": 0.0, "resolver": "place", "state": "NV"} {"aliases": ["lamoille", "lamoille nevada", "lamoille nv"], "attrs": {"land_km2": 4.8, "pop": 0, "type": "cdp"}, "id": "place:3239300", "key": "3239300", "kind": "place", "lat": 40.723853, "lon": -115.478949, "name": "Lamoille", "rank": 0.0, "resolver": "place", "state": "NV"} -{"aliases": ["las vegas", "las vegas nevada", "las vegas nv"], "attrs": {"land_km2": 367.4, "pop": 678922, "type": "city"}, "id": "place:3240000", "key": "3240000", "kind": "place", "lat": 36.233499, "lon": -115.264037, "name": "Las Vegas", "rank": 5.83, "resolver": "place", "state": "NV"} +{"aliases": ["las vegas", "las vegas nevada", "las vegas nv", "sin city", "vegas"], "attrs": {"land_km2": 367.4, "pop": 678922, "type": "city"}, "id": "place:3240000", "key": "3240000", "kind": "place", "lat": 36.233499, "lon": -115.264037, "name": "Las Vegas", "rank": 5.83, "resolver": "place", "state": "NV"} {"aliases": ["laughlin", "laughlin nevada", "laughlin nv"], "attrs": {"land_km2": 42.3, "pop": 0, "type": "cdp"}, "id": "place:3241000", "key": "3241000", "kind": "place", "lat": 35.14039, "lon": -114.616564, "name": "Laughlin", "rank": 0.0, "resolver": "place", "state": "NV"} {"aliases": ["lemmon valley", "lemmon valley nevada", "lemmon valley nv"], "attrs": {"land_km2": 41.3, "pop": 0, "type": "cdp"}, "id": "place:3241800", "key": "3241800", "kind": "place", "lat": 39.687444, "lon": -119.833195, "name": "Lemmon Valley", "rank": 0.0, "resolver": "place", "state": "NV"} {"aliases": ["logan creek", "logan creek nevada", "logan creek nv"], "attrs": {"land_km2": 4.5, "pop": 0, "type": "cdp"}, "id": "place:3242350", "key": "3242350", "kind": "place", "lat": 39.06507, "lon": -119.92449, "name": "Logan Creek", "rank": 0.0, "resolver": "place", "state": "NV"} @@ -17849,7 +17849,7 @@ {"aliases": ["alamillo", "alamillo new mexico", "alamillo nm"], "attrs": {"land_km2": 3.1, "pop": 0, "type": "cdp"}, "id": "place:3501640", "key": "3501640", "kind": "place", "lat": 34.251883, "lon": -106.9158, "name": "Alamillo", "rank": 0.0, "resolver": "place", "state": "NM"} {"aliases": ["alamo", "alamo new mexico", "alamo nm"], "attrs": {"land_km2": 101.5, "pop": 0, "type": "cdp"}, "id": "place:3501710", "key": "3501710", "kind": "place", "lat": 34.4184, "lon": -107.512485, "name": "Alamo", "rank": 0.0, "resolver": "place", "state": "NM"} {"aliases": ["alamogordo", "alamogordo new mexico", "alamogordo nm"], "attrs": {"land_km2": 55.9, "pop": 31667, "type": "city"}, "id": "place:3501780", "key": "3501780", "kind": "place", "lat": 32.883619, "lon": -105.963607, "name": "Alamogordo", "rank": 4.5, "resolver": "place", "state": "NM"} -{"aliases": ["albuquerque", "albuquerque new mexico", "albuquerque nm"], "attrs": {"land_km2": 485.0, "pop": 560326, "type": "city"}, "id": "place:3502000", "key": "3502000", "kind": "place", "lat": 35.10478, "lon": -106.646809, "name": "Albuquerque", "rank": 5.75, "resolver": "place", "state": "NM"} +{"aliases": ["abq", "albuquerque", "albuquerque new mexico", "albuquerque nm", "burque", "duke city"], "attrs": {"land_km2": 485.0, "pop": 560326, "type": "city"}, "id": "place:3502000", "key": "3502000", "kind": "place", "lat": 35.10478, "lon": -106.646809, "name": "Albuquerque", "rank": 5.75, "resolver": "place", "state": "NM"} {"aliases": ["alcalde", "alcalde new mexico", "alcalde nm"], "attrs": {"land_km2": 1.3, "pop": 0, "type": "cdp"}, "id": "place:3502070", "key": "3502070", "kind": "place", "lat": 36.085393, "lon": -106.057516, "name": "Alcalde", "rank": 0.0, "resolver": "place", "state": "NM"} {"aliases": ["algodones", "algodones new mexico", "algodones nm"], "attrs": {"land_km2": 18.9, "pop": 0, "type": "cdp"}, "id": "place:3502210", "key": "3502210", "kind": "place", "lat": 35.370711, "lon": -106.488878, "name": "Algodones", "rank": 0.0, "resolver": "place", "state": "NM"} {"aliases": ["alma", "alma new mexico", "alma nm"], "attrs": {"land_km2": 8.4, "pop": 0, "type": "cdp"}, "id": "place:3502560", "key": "3502560", "kind": "place", "lat": 33.384508, "lon": -108.894402, "name": "Alma", "rank": 0.0, "resolver": "place", "state": "NM"} @@ -18327,7 +18327,7 @@ {"aliases": ["tres arroyos", "tres arroyos new mexico", "tres arroyos nm"], "attrs": {"land_km2": 23.9, "pop": 0, "type": "cdp"}, "id": "place:3579500", "key": "3579500", "kind": "place", "lat": 35.679822, "lon": -106.03986, "name": "Tres Arroyos", "rank": 0.0, "resolver": "place", "state": "NM"} {"aliases": ["trout valley", "trout valley new mexico", "trout valley nm"], "attrs": {"land_km2": 0.5, "pop": 0, "type": "cdp"}, "id": "place:3579680", "key": "3579680", "kind": "place", "lat": 33.036224, "lon": -108.189261, "name": "Trout Valley", "rank": 0.0, "resolver": "place", "state": "NM"} {"aliases": ["truchas", "truchas new mexico", "truchas nm"], "attrs": {"land_km2": 17.9, "pop": 0, "type": "cdp"}, "id": "place:3579700", "key": "3579700", "kind": "place", "lat": 36.036692, "lon": -105.812546, "name": "Truchas", "rank": 0.0, "resolver": "place", "state": "NM"} -{"aliases": ["truth or consequences", "truth or consequences new mexico", "truth or consequences nm"], "attrs": {"land_km2": 72.5, "pop": 5942, "type": "city"}, "id": "place:3579840", "key": "3579840", "kind": "place", "lat": 33.23764, "lon": -107.276237, "name": "Truth or Consequences", "rank": 3.77, "resolver": "place", "state": "NM"} +{"aliases": ["t or c", "truth or consequences", "truth or consequences new mexico", "truth or consequences nm"], "attrs": {"land_km2": 72.5, "pop": 5942, "type": "city"}, "id": "place:3579840", "key": "3579840", "kind": "place", "lat": 33.23764, "lon": -107.276237, "name": "Truth or Consequences", "rank": 3.77, "resolver": "place", "state": "NM"} {"aliases": ["tse bonito", "tse bonito new mexico", "tse bonito nm"], "attrs": {"land_km2": 17.6, "pop": 0, "type": "cdp"}, "id": "place:3579885", "key": "3579885", "kind": "place", "lat": 35.646561, "lon": -109.007056, "name": "Tse Bonito", "rank": 0.0, "resolver": "place", "state": "NM"} {"aliases": ["tucumcari", "tucumcari new mexico", "tucumcari nm"], "attrs": {"land_km2": 24.6, "pop": 5058, "type": "city"}, "id": "place:3579910", "key": "3579910", "kind": "place", "lat": 35.170291, "lon": -103.705046, "name": "Tucumcari", "rank": 3.7, "resolver": "place", "state": "NM"} {"aliases": ["tularosa", "tularosa new mexico", "tularosa nm"], "attrs": {"land_km2": 7.3, "pop": 2632, "type": "village"}, "id": "place:3579980", "key": "3579980", "kind": "place", "lat": 33.075001, "lon": -106.017299, "name": "Tularosa", "rank": 3.42, "resolver": "place", "state": "NM"} @@ -19136,7 +19136,7 @@ {"aliases": ["new square", "new square new york", "new square ny"], "attrs": {"land_km2": 0.9, "pop": 9973, "type": "village"}, "id": "place:3650705", "key": "3650705", "kind": "place", "lat": 41.141022, "lon": -74.029362, "name": "New Square", "rank": 4.0, "resolver": "place", "state": "NY"} {"aliases": ["new suffolk", "new suffolk new york", "new suffolk ny"], "attrs": {"land_km2": 1.4, "pop": 0, "type": "cdp"}, "id": "place:3650727", "key": "3650727", "kind": "place", "lat": 40.996754, "lon": -72.477472, "name": "New Suffolk", "rank": 0.0, "resolver": "place", "state": "NY"} {"aliases": ["new windsor", "new windsor new york", "new windsor ny"], "attrs": {"land_km2": 9.7, "pop": 0, "type": "cdp"}, "id": "place:3650837", "key": "3650837", "kind": "place", "lat": 41.46983, "lon": -74.031832, "name": "New Windsor", "rank": 0.0, "resolver": "place", "state": "NY"} -{"aliases": ["new york", "new york new york", "new york ny"], "attrs": {"land_km2": 778.2, "pop": 8478072, "type": "city"}, "id": "place:3651000", "key": "3651000", "kind": "place", "lat": 40.662712, "lon": -73.938677, "name": "New York", "rank": 6.93, "resolver": "place", "state": "NY"} +{"aliases": ["big apple", "new york", "new york city", "new york new york", "new york ny", "nyc"], "attrs": {"land_km2": 778.2, "pop": 8478072, "type": "city"}, "id": "place:3651000", "key": "3651000", "kind": "place", "lat": 40.662712, "lon": -73.938677, "name": "New York", "rank": 6.93, "resolver": "place", "state": "NY"} {"aliases": ["new york mills", "new york mills new york", "new york mills ny"], "attrs": {"land_km2": 3.1, "pop": 3197, "type": "village"}, "id": "place:3651011", "key": "3651011", "kind": "place", "lat": 43.100709, "lon": -75.293111, "name": "New York Mills", "rank": 3.5, "resolver": "place", "state": "NY"} {"aliases": ["niagara falls", "niagara falls new york", "niagara falls ny"], "attrs": {"land_km2": 36.5, "pop": 47512, "type": "city"}, "id": "place:3651055", "key": "3651055", "kind": "place", "lat": 43.092841, "lon": -79.014743, "name": "Niagara Falls", "rank": 4.68, "resolver": "place", "state": "NY"} {"aliases": ["niagara university", "niagara university new york", "niagara university ny"], "attrs": {"land_km2": 0.6, "pop": 0, "type": "cdp"}, "id": "place:3651088", "key": "3651088", "kind": "place", "lat": 43.137212, "lon": -79.035127, "name": "Niagara University", "rank": 0.0, "resolver": "place", "state": "NY"} @@ -19792,7 +19792,7 @@ {"aliases": ["cerro gordo", "cerro gordo nc", "cerro gordo north carolina"], "attrs": {"land_km2": 2.1, "pop": 143, "type": "town"}, "id": "place:3711620", "key": "3711620", "kind": "place", "lat": 34.322589, "lon": -78.928716, "name": "Cerro Gordo", "rank": 2.16, "resolver": "place", "state": "NC"} {"aliases": ["chadbourn", "chadbourn nc", "chadbourn north carolina"], "attrs": {"land_km2": 6.7, "pop": 1528, "type": "town"}, "id": "place:3711640", "key": "3711640", "kind": "place", "lat": 34.325166, "lon": -78.824983, "name": "Chadbourn", "rank": 3.18, "resolver": "place", "state": "NC"} {"aliases": ["chapel hill", "chapel hill nc", "chapel hill north carolina"], "attrs": {"land_km2": 56.2, "pop": 64028, "type": "town"}, "id": "place:3711800", "key": "3711800", "kind": "place", "lat": 35.928328, "lon": -79.041375, "name": "Chapel Hill", "rank": 4.81, "resolver": "place", "state": "NC"} -{"aliases": ["charlotte", "charlotte nc", "charlotte north carolina"], "attrs": {"land_km2": 807.1, "pop": 943476, "type": "city"}, "id": "place:3712000", "key": "3712000", "kind": "place", "lat": 35.209045, "lon": -80.83099, "name": "Charlotte", "rank": 5.97, "resolver": "place", "state": "NC"} +{"aliases": ["charlotte", "charlotte nc", "charlotte north carolina", "clt", "hornet s nest"], "attrs": {"land_km2": 807.1, "pop": 943476, "type": "city"}, "id": "place:3712000", "key": "3712000", "kind": "place", "lat": 35.209045, "lon": -80.83099, "name": "Charlotte", "rank": 5.97, "resolver": "place", "state": "NC"} {"aliases": ["cherokee", "cherokee nc", "cherokee north carolina"], "attrs": {"land_km2": 31.3, "pop": 0, "type": "cdp"}, "id": "place:3712140", "key": "3712140", "kind": "place", "lat": 35.48594, "lon": -83.301107, "name": "Cherokee", "rank": 0.0, "resolver": "place", "state": "NC"} {"aliases": ["cherry branch", "cherry branch nc", "cherry branch north carolina"], "attrs": {"land_km2": 3.0, "pop": 0, "type": "cdp"}, "id": "place:3712186", "key": "3712186", "kind": "place", "lat": 34.934316, "lon": -76.811196, "name": "Cherry Branch", "rank": 0.0, "resolver": "place", "state": "NC"} {"aliases": ["cherryville", "cherryville nc", "cherryville north carolina"], "attrs": {"land_km2": 15.0, "pop": 6385, "type": "city"}, "id": "place:3712340", "key": "3712340", "kind": "place", "lat": 35.384566, "lon": -81.378942, "name": "Cherryville", "rank": 3.81, "resolver": "place", "state": "NC"} @@ -20354,7 +20354,7 @@ {"aliases": ["taylorsville", "taylorsville nc", "taylorsville north carolina"], "attrs": {"land_km2": 6.2, "pop": 2294, "type": "town"}, "id": "place:3766960", "key": "3766960", "kind": "place", "lat": 35.916779, "lon": -81.175951, "name": "Taylorsville", "rank": 3.36, "resolver": "place", "state": "NC"} {"aliases": ["taylortown", "taylortown nc", "taylortown north carolina"], "attrs": {"land_km2": 3.5, "pop": 687, "type": "town"}, "id": "place:3766980", "key": "3766980", "kind": "place", "lat": 35.214623, "lon": -79.491651, "name": "Taylortown", "rank": 2.84, "resolver": "place", "state": "NC"} {"aliases": ["teachey", "teachey nc", "teachey north carolina"], "attrs": {"land_km2": 2.7, "pop": 483, "type": "town"}, "id": "place:3767000", "key": "3767000", "kind": "place", "lat": 34.770812, "lon": -78.005985, "name": "Teachey", "rank": 2.68, "resolver": "place", "state": "NC"} -{"aliases": ["thomasville", "thomasville nc", "thomasville north carolina"], "attrs": {"land_km2": 43.7, "pop": 27605, "type": "city"}, "id": "place:3767420", "key": "3767420", "kind": "place", "lat": 35.881308, "lon": -80.081549, "name": "Thomasville", "rank": 4.44, "resolver": "place", "state": "NC"} +{"aliases": ["chair city", "t vegas", "tater town", "thomasville", "thomasville nc", "thomasville north carolina", "tville"], "attrs": {"land_km2": 43.7, "pop": 27605, "type": "city"}, "id": "place:3767420", "key": "3767420", "kind": "place", "lat": 35.881308, "lon": -80.081549, "name": "Thomasville", "rank": 4.44, "resolver": "place", "state": "NC"} {"aliases": ["toast", "toast nc", "toast north carolina"], "attrs": {"land_km2": 3.9, "pop": 0, "type": "cdp"}, "id": "place:3767700", "key": "3767700", "kind": "place", "lat": 36.496679, "lon": -80.635453, "name": "Toast", "rank": 0.0, "resolver": "place", "state": "NC"} {"aliases": ["tobaccoville", "tobaccoville nc", "tobaccoville north carolina"], "attrs": {"land_km2": 19.8, "pop": 2740, "type": "village"}, "id": "place:3767720", "key": "3767720", "kind": "place", "lat": 36.225597, "lon": -80.358846, "name": "Tobaccoville", "rank": 3.44, "resolver": "place", "state": "NC"} {"aliases": ["topsail beach", "topsail beach nc", "topsail beach north carolina"], "attrs": {"land_km2": 11.4, "pop": 500, "type": "town"}, "id": "place:3768040", "key": "3768040", "kind": "place", "lat": 34.366371, "lon": -77.647572, "name": "Topsail Beach", "rank": 2.7, "resolver": "place", "state": "NC"} @@ -20419,7 +20419,7 @@ {"aliases": ["whittier", "whittier nc", "whittier north carolina"], "attrs": {"land_km2": 0.5, "pop": 0, "type": "cdp"}, "id": "place:3773780", "key": "3773780", "kind": "place", "lat": 35.434676, "lon": -83.354758, "name": "Whittier", "rank": 0.0, "resolver": "place", "state": "NC"} {"aliases": ["wilkesboro", "wilkesboro nc", "wilkesboro north carolina"], "attrs": {"land_km2": 17.1, "pop": 3620, "type": "town"}, "id": "place:3774020", "key": "3774020", "kind": "place", "lat": 36.142425, "lon": -81.176895, "name": "Wilkesboro", "rank": 3.56, "resolver": "place", "state": "NC"} {"aliases": ["williamston", "williamston nc", "williamston north carolina"], "attrs": {"land_km2": 11.7, "pop": 5066, "type": "town"}, "id": "place:3774220", "key": "3774220", "kind": "place", "lat": 35.846539, "lon": -77.066078, "name": "Williamston", "rank": 3.7, "resolver": "place", "state": "NC"} -{"aliases": ["wilmington", "wilmington nc", "wilmington north carolina"], "attrs": {"land_km2": 133.0, "pop": 125284, "type": "city"}, "id": "place:3774440", "key": "3774440", "kind": "place", "lat": 34.209225, "lon": -77.885767, "name": "Wilmington", "rank": 5.1, "resolver": "place", "state": "NC"} +{"aliases": ["port city", "wilmington", "wilmington nc", "wilmington north carolina"], "attrs": {"land_km2": 133.0, "pop": 125284, "type": "city"}, "id": "place:3774440", "key": "3774440", "kind": "place", "lat": 34.209225, "lon": -77.885767, "name": "Wilmington", "rank": 5.1, "resolver": "place", "state": "NC"} {"aliases": ["wilson", "wilson nc", "wilson north carolina"], "attrs": {"land_km2": 81.8, "pop": 48579, "type": "city"}, "id": "place:3774540", "key": "3774540", "kind": "place", "lat": 35.731488, "lon": -77.93075, "name": "Wilson", "rank": 4.69, "resolver": "place", "state": "NC"} {"aliases": ["wilson s mills", "wilson s mills nc", "wilson s mills north carolina"], "attrs": {"land_km2": 16.5, "pop": 2913, "type": "town"}, "id": "place:3774580", "key": "3774580", "kind": "place", "lat": 35.586155, "lon": -78.362987, "name": "Wilson's Mills", "rank": 3.46, "resolver": "place", "state": "NC"} {"aliases": ["windsor", "windsor nc", "windsor north carolina"], "attrs": {"land_km2": 7.3, "pop": 3268, "type": "town"}, "id": "place:3774680", "key": "3774680", "kind": "place", "lat": 35.993046, "lon": -76.939899, "name": "Windsor", "rank": 3.51, "resolver": "place", "state": "NC"} @@ -21059,7 +21059,7 @@ {"aliases": ["choctaw lake", "choctaw lake oh", "choctaw lake ohio"], "attrs": {"land_km2": 2.7, "pop": 0, "type": "cdp"}, "id": "place:3914288", "key": "3914288", "kind": "place", "lat": 39.959359, "lon": -83.490098, "name": "Choctaw Lake", "rank": 0.0, "resolver": "place", "state": "OH"} {"aliases": ["christiansburg", "christiansburg oh", "christiansburg ohio"], "attrs": {"land_km2": 0.6, "pop": 503, "type": "village"}, "id": "place:3914296", "key": "3914296", "kind": "place", "lat": 40.056747, "lon": -84.025536, "name": "Christiansburg", "rank": 2.7, "resolver": "place", "state": "OH"} {"aliases": ["churchill", "churchill oh", "churchill ohio"], "attrs": {"land_km2": 6.5, "pop": 0, "type": "cdp"}, "id": "place:3914324", "key": "3914324", "kind": "place", "lat": 41.169456, "lon": -80.665973, "name": "Churchill", "rank": 0.0, "resolver": "place", "state": "OH"} -{"aliases": ["cincinnati", "cincinnati oh", "cincinnati ohio"], "attrs": {"land_km2": 201.8, "pop": 314915, "type": "city"}, "id": "place:3915000", "key": "3915000", "kind": "place", "lat": 39.140183, "lon": -84.505829, "name": "Cincinnati", "rank": 5.5, "resolver": "place", "state": "OH"} +{"aliases": ["cincinnati", "cincinnati oh", "cincinnati ohio", "cincy"], "attrs": {"land_km2": 201.8, "pop": 314915, "type": "city"}, "id": "place:3915000", "key": "3915000", "kind": "place", "lat": 39.140183, "lon": -84.505829, "name": "Cincinnati", "rank": 5.5, "resolver": "place", "state": "OH"} {"aliases": ["cinnamon lake", "cinnamon lake oh", "cinnamon lake ohio"], "attrs": {"land_km2": 4.2, "pop": 0, "type": "cdp"}, "id": "place:3915008", "key": "3915008", "kind": "place", "lat": 40.985996, "lon": -82.19673, "name": "Cinnamon Lake", "rank": 0.0, "resolver": "place", "state": "OH"} {"aliases": ["circleville", "circleville oh", "circleville ohio"], "attrs": {"land_km2": 18.7, "pop": 14433, "type": "city"}, "id": "place:3915070", "key": "3915070", "kind": "place", "lat": 39.601152, "lon": -82.932515, "name": "Circleville", "rank": 4.16, "resolver": "place", "state": "OH"} {"aliases": ["clarington", "clarington oh", "clarington ohio"], "attrs": {"land_km2": 3.0, "pop": 271, "type": "village"}, "id": "place:3915210", "key": "3915210", "kind": "place", "lat": 39.780856, "lon": -80.864985, "name": "Clarington", "rank": 2.43, "resolver": "place", "state": "OH"} @@ -21082,7 +21082,7 @@ {"aliases": ["college corner", "college corner oh", "college corner ohio"], "attrs": {"land_km2": 0.6, "pop": 397, "type": "village"}, "id": "place:3916700", "key": "3916700", "kind": "place", "lat": 39.568912, "lon": -84.81141, "name": "College Corner", "rank": 2.6, "resolver": "place", "state": "OH"} {"aliases": ["collins", "collins oh", "collins ohio"], "attrs": {"land_km2": 12.0, "pop": 0, "type": "cdp"}, "id": "place:3916784", "key": "3916784", "kind": "place", "lat": 41.254105, "lon": -82.482363, "name": "Collins", "rank": 0.0, "resolver": "place", "state": "OH"} {"aliases": ["columbiana", "columbiana oh", "columbiana ohio"], "attrs": {"land_km2": 15.8, "pop": 6773, "type": "city"}, "id": "place:3917036", "key": "3917036", "kind": "place", "lat": 40.885537, "lon": -80.667818, "name": "Columbiana", "rank": 3.83, "resolver": "place", "state": "OH"} -{"aliases": ["columbus", "columbus oh", "columbus ohio"], "attrs": {"land_km2": 573.8, "pop": 933263, "type": "city"}, "id": "place:3918000", "key": "3918000", "kind": "place", "lat": 39.983938, "lon": -82.984882, "name": "Columbus", "rank": 5.97, "resolver": "place", "state": "OH"} +{"aliases": ["cbus", "columbus", "columbus oh", "columbus ohio"], "attrs": {"land_km2": 573.8, "pop": 933263, "type": "city"}, "id": "place:3918000", "key": "3918000", "kind": "place", "lat": 39.983938, "lon": -82.984882, "name": "Columbus", "rank": 5.97, "resolver": "place", "state": "OH"} {"aliases": ["columbus grove", "columbus grove oh", "columbus grove ohio"], "attrs": {"land_km2": 3.2, "pop": 2124, "type": "village"}, "id": "place:3918014", "key": "3918014", "kind": "place", "lat": 40.919829, "lon": -84.060307, "name": "Columbus Grove", "rank": 3.33, "resolver": "place", "state": "OH"} {"aliases": ["commercial point", "commercial point oh", "commercial point ohio"], "attrs": {"land_km2": 11.0, "pop": 3221, "type": "village"}, "id": "place:3918070", "key": "3918070", "kind": "place", "lat": 39.790372, "lon": -83.018421, "name": "Commercial Point", "rank": 3.51, "resolver": "place", "state": "OH"} {"aliases": ["concorde hills", "concorde hills oh", "concorde hills ohio"], "attrs": {"land_km2": 0.9, "pop": 0, "type": "cdp"}, "id": "place:3918244", "key": "3918244", "kind": "place", "lat": 39.210768, "lon": -84.357332, "name": "Concorde Hills", "rank": 0.0, "resolver": "place", "state": "OH"} @@ -21196,7 +21196,7 @@ {"aliases": ["fayette", "fayette oh", "fayette ohio"], "attrs": {"land_km2": 2.5, "pop": 1282, "type": "village"}, "id": "place:3926768", "key": "3926768", "kind": "place", "lat": 41.672755, "lon": -84.328428, "name": "Fayette", "rank": 3.11, "resolver": "place", "state": "OH"} {"aliases": ["fayetteville", "fayetteville oh", "fayetteville ohio"], "attrs": {"land_km2": 1.2, "pop": 317, "type": "village"}, "id": "place:3926796", "key": "3926796", "kind": "place", "lat": 39.185373, "lon": -83.931964, "name": "Fayetteville", "rank": 2.5, "resolver": "place", "state": "OH"} {"aliases": ["felicity", "felicity oh", "felicity ohio"], "attrs": {"land_km2": 0.7, "pop": 661, "type": "village"}, "id": "place:3926880", "key": "3926880", "kind": "place", "lat": 38.838763, "lon": -84.098458, "name": "Felicity", "rank": 2.82, "resolver": "place", "state": "OH"} -{"aliases": ["findlay", "findlay oh", "findlay ohio"], "attrs": {"land_km2": 50.9, "pop": 40343, "type": "city"}, "id": "place:3927048", "key": "3927048", "kind": "place", "lat": 41.047364, "lon": -83.638093, "name": "Findlay", "rank": 4.61, "resolver": "place", "state": "OH"} +{"aliases": ["findlay", "findlay oh", "findlay ohio", "flag city usa"], "attrs": {"land_km2": 50.9, "pop": 40343, "type": "city"}, "id": "place:3927048", "key": "3927048", "kind": "place", "lat": 41.047364, "lon": -83.638093, "name": "Findlay", "rank": 4.61, "resolver": "place", "state": "OH"} {"aliases": ["finneytown", "finneytown oh", "finneytown ohio"], "attrs": {"land_km2": 10.3, "pop": 0, "type": "cdp"}, "id": "place:3927104", "key": "3927104", "kind": "place", "lat": 39.215972, "lon": -84.514556, "name": "Finneytown", "rank": 0.0, "resolver": "place", "state": "OH"} {"aliases": ["five points", "five points oh", "five points ohio"], "attrs": {"land_km2": 5.3, "pop": 0, "type": "cdp"}, "id": "place:3927303", "key": "3927303", "kind": "place", "lat": 39.559486, "lon": -84.186781, "name": "Five Points", "rank": 0.0, "resolver": "place", "state": "OH"} {"aliases": ["flat rock", "flat rock oh", "flat rock ohio"], "attrs": {"land_km2": 0.5, "pop": 0, "type": "cdp"}, "id": "place:3927356", "key": "3927356", "kind": "place", "lat": 41.235467, "lon": -82.859197, "name": "Flat Rock", "rank": 0.0, "resolver": "place", "state": "OH"} @@ -22673,7 +22673,7 @@ {"aliases": ["okay", "okay ok", "okay oklahoma"], "attrs": {"land_km2": 2.1, "pop": 585, "type": "town"}, "id": "place:4054100", "key": "4054100", "kind": "place", "lat": 35.865019, "lon": -95.307735, "name": "Okay", "rank": 2.77, "resolver": "place", "state": "OK"} {"aliases": ["okeene", "okeene ok", "okeene oklahoma"], "attrs": {"land_km2": 5.9, "pop": 1052, "type": "town"}, "id": "place:4054150", "key": "4054150", "kind": "place", "lat": 36.117076, "lon": -98.316734, "name": "Okeene", "rank": 3.02, "resolver": "place", "state": "OK"} {"aliases": ["okemah", "okemah ok", "okemah oklahoma"], "attrs": {"land_km2": 8.0, "pop": 3066, "type": "city"}, "id": "place:4054200", "key": "4054200", "kind": "place", "lat": 35.429269, "lon": -96.300309, "name": "Okemah", "rank": 3.49, "resolver": "place", "state": "OK"} -{"aliases": ["oklahoma city", "oklahoma city ok", "oklahoma city oklahoma"], "attrs": {"land_km2": 1570.8, "pop": 712919, "type": "city"}, "id": "place:4055000", "key": "4055000", "kind": "place", "lat": 35.467079, "lon": -97.513657, "name": "Oklahoma City", "rank": 5.85, "resolver": "place", "state": "OK"} +{"aliases": ["okc", "oklahoma city", "oklahoma city ok", "oklahoma city oklahoma"], "attrs": {"land_km2": 1570.8, "pop": 712919, "type": "city"}, "id": "place:4055000", "key": "4055000", "kind": "place", "lat": 35.467079, "lon": -97.513657, "name": "Oklahoma City", "rank": 5.85, "resolver": "place", "state": "OK"} {"aliases": ["okmulgee", "okmulgee ok", "okmulgee oklahoma"], "attrs": {"land_km2": 44.6, "pop": 11354, "type": "city"}, "id": "place:4055150", "key": "4055150", "kind": "place", "lat": 35.625162, "lon": -95.954032, "name": "Okmulgee", "rank": 4.06, "resolver": "place", "state": "OK"} {"aliases": ["oktaha", "oktaha ok", "oktaha oklahoma"], "attrs": {"land_km2": 0.7, "pop": 347, "type": "town"}, "id": "place:4055200", "key": "4055200", "kind": "place", "lat": 35.577489, "lon": -95.477892, "name": "Oktaha", "rank": 2.54, "resolver": "place", "state": "OK"} {"aliases": ["old eucha", "old eucha ok", "old eucha oklahoma"], "attrs": {"land_km2": 8.0, "pop": 0, "type": "cdp"}, "id": "place:4055275", "key": "4055275", "kind": "place", "lat": 36.355135, "lon": -94.937907, "name": "Old Eucha", "rank": 0.0, "resolver": "place", "state": "OK"} @@ -23251,7 +23251,7 @@ {"aliases": ["pistol river", "pistol river or", "pistol river oregon"], "attrs": {"land_km2": 1.5, "pop": 0, "type": "cdp"}, "id": "place:4158050", "key": "4158050", "kind": "place", "lat": 42.287853, "lon": -124.398775, "name": "Pistol River", "rank": 0.0, "resolver": "place", "state": "OR"} {"aliases": ["pleasant valley", "pleasant valley or", "pleasant valley oregon"], "attrs": {"land_km2": 7.6, "pop": 0, "type": "cdp"}, "id": "place:4158500", "key": "4158500", "kind": "place", "lat": 45.376125, "lon": -123.795172, "name": "Pleasant Valley", "rank": 0.0, "resolver": "place", "state": "OR"} {"aliases": ["plush", "plush or", "plush oregon"], "attrs": {"land_km2": 4.2, "pop": 0, "type": "cdp"}, "id": "place:4158550", "key": "4158550", "kind": "place", "lat": 42.405043, "lon": -119.899312, "name": "Plush", "rank": 0.0, "resolver": "place", "state": "OR"} -{"aliases": ["portland", "portland or", "portland oregon"], "attrs": {"land_km2": 345.7, "pop": 635749, "type": "city"}, "id": "place:4159000", "key": "4159000", "kind": "place", "lat": 45.536951, "lon": -122.649971, "name": "Portland", "rank": 5.8, "resolver": "place", "state": "OR"} +{"aliases": ["pdx", "portland", "portland or", "portland oregon"], "attrs": {"land_km2": 345.7, "pop": 635749, "type": "city"}, "id": "place:4159000", "key": "4159000", "kind": "place", "lat": 45.536951, "lon": -122.649971, "name": "Portland", "rank": 5.8, "resolver": "place", "state": "OR"} {"aliases": ["port orford", "port orford or", "port orford oregon"], "attrs": {"land_km2": 4.1, "pop": 1114, "type": "city"}, "id": "place:4159250", "key": "4159250", "kind": "place", "lat": 42.749276, "lon": -124.498118, "name": "Port Orford", "rank": 3.05, "resolver": "place", "state": "OR"} {"aliases": ["powers", "powers or", "powers oregon"], "attrs": {"land_km2": 1.5, "pop": 703, "type": "city"}, "id": "place:4159600", "key": "4159600", "kind": "place", "lat": 42.885003, "lon": -124.076248, "name": "Powers", "rank": 2.85, "resolver": "place", "state": "OR"} {"aliases": ["prairie", "prairie city", "prairie city or", "prairie city oregon", "prairie or", "prairie oregon"], "attrs": {"land_km2": 2.6, "pop": 817, "type": "city"}, "id": "place:4159650", "key": "4159650", "kind": "place", "lat": 44.461101, "lon": -118.70938, "name": "Prairie City", "rank": 2.91, "resolver": "place", "state": "OR"} @@ -23889,7 +23889,7 @@ {"aliases": ["elgin", "elgin pa", "elgin pennsylvania"], "attrs": {"land_km2": 3.8, "pop": 199, "type": "borough"}, "id": "place:4222960", "key": "4222960", "kind": "place", "lat": 41.907013, "lon": -79.746508, "name": "Elgin", "rank": 2.3, "resolver": "place", "state": "PA"} {"aliases": ["elim", "elim pa", "elim pennsylvania"], "attrs": {"land_km2": 5.2, "pop": 0, "type": "cdp"}, "id": "place:4222976", "key": "4222976", "kind": "place", "lat": 40.297248, "lon": -78.945469, "name": "Elim", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["elizabeth", "elizabeth pa", "elizabeth pennsylvania"], "attrs": {"land_km2": 0.9, "pop": 1357, "type": "borough"}, "id": "place:4222992", "key": "4222992", "kind": "place", "lat": 40.271554, "lon": -79.887153, "name": "Elizabeth", "rank": 3.13, "resolver": "place", "state": "PA"} -{"aliases": ["elizabethtown", "elizabethtown pa", "elizabethtown pennsylvania"], "attrs": {"land_km2": 6.9, "pop": 11993, "type": "borough"}, "id": "place:4223016", "key": "4223016", "kind": "place", "lat": 40.153284, "lon": -76.598652, "name": "Elizabethtown", "rank": 4.08, "resolver": "place", "state": "PA"} +{"aliases": ["e town", "elizabethtown", "elizabethtown pa", "elizabethtown pennsylvania"], "attrs": {"land_km2": 6.9, "pop": 11993, "type": "borough"}, "id": "place:4223016", "key": "4223016", "kind": "place", "lat": 40.153284, "lon": -76.598652, "name": "Elizabethtown", "rank": 4.08, "resolver": "place", "state": "PA"} {"aliases": ["elizabethville", "elizabethville pa", "elizabethville pennsylvania"], "attrs": {"land_km2": 1.4, "pop": 1352, "type": "borough"}, "id": "place:4223024", "key": "4223024", "kind": "place", "lat": 40.547242, "lon": -76.815973, "name": "Elizabethville", "rank": 3.13, "resolver": "place", "state": "PA"} {"aliases": ["elk grove", "elk grove pa", "elk grove pennsylvania"], "attrs": {"land_km2": 1.1, "pop": 0, "type": "cdp"}, "id": "place:4223112", "key": "4223112", "kind": "place", "lat": 41.306567, "lon": -76.408816, "name": "Elk Grove", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["elkins park", "elkins park pa", "elkins park pennsylvania"], "attrs": {"land_km2": 4.5, "pop": 0, "type": "cdp"}, "id": "place:4223128", "key": "4223128", "kind": "place", "lat": 40.077471, "lon": -75.128024, "name": "Elkins Park", "rank": 0.0, "resolver": "place", "state": "PA"} @@ -24247,7 +24247,7 @@ {"aliases": ["kerrtown", "kerrtown pa", "kerrtown pennsylvania"], "attrs": {"land_km2": 2.3, "pop": 0, "type": "cdp"}, "id": "place:4239504", "key": "4239504", "kind": "place", "lat": 41.626643, "lon": -80.169107, "name": "Kerrtown", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["kersey", "kersey pa", "kersey pennsylvania"], "attrs": {"land_km2": 4.0, "pop": 0, "type": "cdp"}, "id": "place:4239512", "key": "4239512", "kind": "place", "lat": 41.357261, "lon": -78.609208, "name": "Kersey", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["kimberton", "kimberton pa", "kimberton pennsylvania"], "attrs": {"land_km2": 1.2, "pop": 0, "type": "cdp"}, "id": "place:4239648", "key": "4239648", "kind": "place", "lat": 40.132126, "lon": -75.573667, "name": "Kimberton", "rank": 0.0, "resolver": "place", "state": "PA"} -{"aliases": ["king of prussia", "king of prussia pa", "king of prussia pennsylvania"], "attrs": {"land_km2": 22.0, "pop": 0, "type": "cdp"}, "id": "place:4239736", "key": "4239736", "kind": "place", "lat": 40.096029, "lon": -75.379074, "name": "King of Prussia", "rank": 0.0, "resolver": "place", "state": "PA"} +{"aliases": ["king of prussia", "king of prussia pa", "king of prussia pennsylvania", "kop"], "attrs": {"land_km2": 22.0, "pop": 0, "type": "cdp"}, "id": "place:4239736", "key": "4239736", "kind": "place", "lat": 40.096029, "lon": -75.379074, "name": "King of Prussia", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["kingston", "kingston pa", "kingston pennsylvania"], "attrs": {"land_km2": 5.6, "pop": 13476, "type": "borough"}, "id": "place:4239784", "key": "4239784", "kind": "place", "lat": 41.264754, "lon": -75.886574, "name": "Kingston", "rank": 4.13, "resolver": "place", "state": "PA"} {"aliases": ["kirkwood", "kirkwood pa", "kirkwood pennsylvania"], "attrs": {"land_km2": 5.8, "pop": 0, "type": "cdp"}, "id": "place:4239944", "key": "4239944", "kind": "place", "lat": 39.847079, "lon": -76.083376, "name": "Kirkwood", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["kiskimere", "kiskimere pa", "kiskimere pennsylvania"], "attrs": {"land_km2": 0.8, "pop": 0, "type": "cdp"}, "id": "place:4239960", "key": "4239960", "kind": "place", "lat": 40.619013, "lon": -79.585698, "name": "Kiskimere", "rank": 0.0, "resolver": "place", "state": "PA"} @@ -24747,7 +24747,7 @@ {"aliases": ["peru", "peru pa", "peru pennsylvania"], "attrs": {"land_km2": 0.1, "pop": 0, "type": "cdp"}, "id": "place:4259580", "key": "4259580", "kind": "place", "lat": 40.854377, "lon": -77.767175, "name": "Peru", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["petersburg", "petersburg pa", "petersburg pennsylvania"], "attrs": {"land_km2": 0.9, "pop": 416, "type": "borough"}, "id": "place:4259616", "key": "4259616", "kind": "place", "lat": 40.572598, "lon": -78.050615, "name": "Petersburg", "rank": 2.62, "resolver": "place", "state": "PA"} {"aliases": ["petrolia", "petrolia pa", "petrolia pennsylvania"], "attrs": {"land_km2": 1.0, "pop": 173, "type": "borough"}, "id": "place:4259672", "key": "4259672", "kind": "place", "lat": 41.021619, "lon": -79.716777, "name": "Petrolia", "rank": 2.24, "resolver": "place", "state": "PA"} -{"aliases": ["philadelphia", "philadelphia pa", "philadelphia pennsylvania"], "attrs": {"land_km2": 347.9, "pop": 1573916, "type": "city"}, "id": "place:4260000", "key": "4260000", "kind": "place", "lat": 40.009376, "lon": -75.133346, "name": "Philadelphia", "rank": 6.2, "resolver": "place", "state": "PA"} +{"aliases": ["phila", "philadelphia", "philadelphia pa", "philadelphia pennsylvania", "philly", "phl"], "attrs": {"land_km2": 347.9, "pop": 1573916, "type": "city"}, "id": "place:4260000", "key": "4260000", "kind": "place", "lat": 40.009376, "lon": -75.133346, "name": "Philadelphia", "rank": 6.2, "resolver": "place", "state": "PA"} {"aliases": ["philipsburg", "philipsburg pa", "philipsburg pennsylvania"], "attrs": {"land_km2": 2.3, "pop": 2735, "type": "borough"}, "id": "place:4260008", "key": "4260008", "kind": "place", "lat": 40.892852, "lon": -78.216743, "name": "Philipsburg", "rank": 3.44, "resolver": "place", "state": "PA"} {"aliases": ["phoenixville", "phoenixville pa", "phoenixville pennsylvania"], "attrs": {"land_km2": 9.1, "pop": 20286, "type": "borough"}, "id": "place:4260120", "key": "4260120", "kind": "place", "lat": 40.135201, "lon": -75.522699, "name": "Phoenixville", "rank": 4.31, "resolver": "place", "state": "PA"} {"aliases": ["picture rocks", "picture rocks pa", "picture rocks pennsylvania"], "attrs": {"land_km2": 2.5, "pop": 632, "type": "borough"}, "id": "place:4260136", "key": "4260136", "kind": "place", "lat": 41.277711, "lon": -76.705997, "name": "Picture Rocks", "rank": 2.8, "resolver": "place", "state": "PA"} @@ -24760,7 +24760,7 @@ {"aliases": ["pine ridge", "pine ridge pa", "pine ridge pennsylvania"], "attrs": {"land_km2": 6.1, "pop": 0, "type": "cdp"}, "id": "place:4260547", "key": "4260547", "kind": "place", "lat": 41.144014, "lon": -74.98701, "name": "Pine Ridge", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["pine run", "pine run pa", "pine run pennsylvania"], "attrs": {"land_km2": 2.2, "pop": 0, "type": "cdp"}, "id": "place:4260550", "key": "4260550", "kind": "place", "lat": 40.686433, "lon": -80.23942, "name": "Pine Run", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["pitcairn", "pitcairn pa", "pitcairn pennsylvania"], "attrs": {"land_km2": 1.3, "pop": 2984, "type": "borough"}, "id": "place:4260712", "key": "4260712", "kind": "place", "lat": 40.407774, "lon": -79.776354, "name": "Pitcairn", "rank": 3.47, "resolver": "place", "state": "PA"} -{"aliases": ["pittsburgh", "pittsburgh pa", "pittsburgh pennsylvania"], "attrs": {"land_km2": 143.4, "pop": 307668, "type": "city"}, "id": "place:4261000", "key": "4261000", "kind": "place", "lat": 40.439936, "lon": -79.975676, "name": "Pittsburgh", "rank": 5.49, "resolver": "place", "state": "PA"} +{"aliases": ["pgh", "pittsburgh", "pittsburgh pa", "pittsburgh pennsylvania", "steel city"], "attrs": {"land_km2": 143.4, "pop": 307668, "type": "city"}, "id": "place:4261000", "key": "4261000", "kind": "place", "lat": 40.439936, "lon": -79.975676, "name": "Pittsburgh", "rank": 5.49, "resolver": "place", "state": "PA"} {"aliases": ["pittston", "pittston pa", "pittston pennsylvania"], "attrs": {"land_km2": 4.0, "pop": 7743, "type": "city"}, "id": "place:4261048", "key": "4261048", "kind": "place", "lat": 41.32792, "lon": -75.785684, "name": "Pittston", "rank": 3.89, "resolver": "place", "state": "PA"} {"aliases": ["plainfield", "plainfield pa", "plainfield pennsylvania"], "attrs": {"land_km2": 1.7, "pop": 0, "type": "cdp"}, "id": "place:4261080", "key": "4261080", "kind": "place", "lat": 40.201172, "lon": -77.283279, "name": "Plainfield", "rank": 0.0, "resolver": "place", "state": "PA"} {"aliases": ["plains", "plains pa", "plains pennsylvania"], "attrs": {"land_km2": 3.3, "pop": 0, "type": "cdp"}, "id": "place:4261112", "key": "4261112", "kind": "place", "lat": 41.275791, "lon": -75.851871, "name": "Plains", "rank": 0.0, "resolver": "place", "state": "PA"} @@ -26698,7 +26698,7 @@ {"aliases": ["mowbray mountain", "mowbray mountain tennessee", "mowbray mountain tn"], "attrs": {"land_km2": 52.4, "pop": 0, "type": "cdp"}, "id": "place:4751402", "key": "4751402", "kind": "place", "lat": 35.275117, "lon": -85.222552, "name": "Mowbray Mountain", "rank": 0.0, "resolver": "place", "state": "TN"} {"aliases": ["munford", "munford tennessee", "munford tn"], "attrs": {"land_km2": 24.5, "pop": 6896, "type": "city"}, "id": "place:4751540", "key": "4751540", "kind": "place", "lat": 35.442869, "lon": -89.815922, "name": "Munford", "rank": 3.84, "resolver": "place", "state": "TN"} {"aliases": ["murfreesboro", "murfreesboro tennessee", "murfreesboro tn"], "attrs": {"land_km2": 167.2, "pop": 168387, "type": "city"}, "id": "place:4751560", "key": "4751560", "kind": "place", "lat": 35.853915, "lon": -86.421224, "name": "Murfreesboro", "rank": 5.23, "resolver": "place", "state": "TN"} -{"aliases": ["nashville", "nashville davidson metropolitan government", "nashville davidson metropolitan government tennessee", "nashville davidson metropolitan government tn", "nashville tennessee", "nashville tn"], "attrs": {"land_km2": 1231.7, "pop": 704963, "type": "(balance)"}, "id": "place:4752006", "key": "4752006", "kind": "place", "lat": 36.1718, "lon": -86.785002, "name": "Nashville-Davidson metropolitan government", "rank": 5.85, "resolver": "place", "state": "TN"} +{"aliases": ["music city", "nashvegas", "nashville", "nashville davidson metropolitan government", "nashville davidson metropolitan government tennessee", "nashville davidson metropolitan government tn", "nashville tennessee", "nashville tn"], "attrs": {"land_km2": 1231.7, "pop": 704963, "type": "(balance)"}, "id": "place:4752006", "key": "4752006", "kind": "place", "lat": 36.1718, "lon": -86.785002, "name": "Nashville-Davidson metropolitan government", "rank": 5.85, "resolver": "place", "state": "TN"} {"aliases": ["newbern", "newbern tennessee", "newbern tn"], "attrs": {"land_km2": 11.7, "pop": 3247, "type": "town"}, "id": "place:4752400", "key": "4752400", "kind": "place", "lat": 36.116881, "lon": -89.271363, "name": "Newbern", "rank": 3.51, "resolver": "place", "state": "TN"} {"aliases": ["new deal", "new deal tennessee", "new deal tn"], "attrs": {"land_km2": 4.9, "pop": 0, "type": "cdp"}, "id": "place:4752520", "key": "4752520", "kind": "place", "lat": 36.513864, "lon": -86.562867, "name": "New Deal", "rank": 0.0, "resolver": "place", "state": "TN"} {"aliases": ["new hope", "new hope tennessee", "new hope tn"], "attrs": {"land_km2": 26.8, "pop": 973, "type": "city"}, "id": "place:4752780", "key": "4752780", "kind": "place", "lat": 34.999428, "lon": -85.668442, "name": "New Hope", "rank": 2.99, "resolver": "place", "state": "TN"} @@ -26955,7 +26955,7 @@ {"aliases": ["atlanta", "atlanta texas", "atlanta tx"], "attrs": {"land_km2": 32.4, "pop": 5448, "type": "city"}, "id": "place:4804516", "key": "4804516", "kind": "place", "lat": 33.113238, "lon": -94.16761, "name": "Atlanta", "rank": 3.74, "resolver": "place", "state": "TX"} {"aliases": ["aubrey", "aubrey texas", "aubrey tx"], "attrs": {"land_km2": 9.6, "pop": 8843, "type": "city"}, "id": "place:4804600", "key": "4804600", "kind": "place", "lat": 33.30855, "lon": -96.979088, "name": "Aubrey", "rank": 3.95, "resolver": "place", "state": "TX"} {"aliases": ["aurora", "aurora texas", "aurora tx"], "attrs": {"land_km2": 9.6, "pop": 1618, "type": "city"}, "id": "place:4804672", "key": "4804672", "kind": "place", "lat": 33.055942, "lon": -97.509615, "name": "Aurora", "rank": 3.21, "resolver": "place", "state": "TX"} -{"aliases": ["austin", "austin texas", "austin tx"], "attrs": {"land_km2": 845.3, "pop": 993588, "type": "city"}, "id": "place:4805000", "key": "4805000", "kind": "place", "lat": 30.298622, "lon": -97.754134, "name": "Austin", "rank": 6.0, "resolver": "place", "state": "TX"} +{"aliases": ["atx", "austin", "austin texas", "austin tx"], "attrs": {"land_km2": 845.3, "pop": 993588, "type": "city"}, "id": "place:4805000", "key": "4805000", "kind": "place", "lat": 30.298622, "lon": -97.754134, "name": "Austin", "rank": 6.0, "resolver": "place", "state": "TX"} {"aliases": ["austwell", "austwell texas", "austwell tx"], "attrs": {"land_km2": 0.9, "pop": 114, "type": "city"}, "id": "place:4805036", "key": "4805036", "kind": "place", "lat": 28.391101, "lon": -96.843715, "name": "Austwell", "rank": 2.06, "resolver": "place", "state": "TX"} {"aliases": ["avery", "avery texas", "avery tx"], "attrs": {"land_km2": 2.5, "pop": 447, "type": "town"}, "id": "place:4805072", "key": "4805072", "kind": "place", "lat": 33.551195, "lon": -94.780084, "name": "Avery", "rank": 2.65, "resolver": "place", "state": "TX"} {"aliases": ["avinger", "avinger texas", "avinger tx"], "attrs": {"land_km2": 5.0, "pop": 376, "type": "town"}, "id": "place:4805084", "key": "4805084", "kind": "place", "lat": 32.896894, "lon": -94.552742, "name": "Avinger", "rank": 2.58, "resolver": "place", "state": "TX"} @@ -27267,7 +27267,7 @@ {"aliases": ["daingerfield", "daingerfield texas", "daingerfield tx"], "attrs": {"land_km2": 6.4, "pop": 2534, "type": "city"}, "id": "place:4818464", "key": "4818464", "kind": "place", "lat": 33.030488, "lon": -94.72588, "name": "Daingerfield", "rank": 3.4, "resolver": "place", "state": "TX"} {"aliases": ["daisetta", "daisetta texas", "daisetta tx"], "attrs": {"land_km2": 3.8, "pop": 1177, "type": "city"}, "id": "place:4818476", "key": "4818476", "kind": "place", "lat": 30.114693, "lon": -94.641633, "name": "Daisetta", "rank": 3.07, "resolver": "place", "state": "TX"} {"aliases": ["dalhart", "dalhart texas", "dalhart tx"], "attrs": {"land_km2": 12.4, "pop": 8443, "type": "city"}, "id": "place:4818524", "key": "4818524", "kind": "place", "lat": 36.057423, "lon": -102.512212, "name": "Dalhart", "rank": 3.93, "resolver": "place", "state": "TX"} -{"aliases": ["dallas", "dallas texas", "dallas tx"], "attrs": {"land_km2": 879.8, "pop": 1326087, "type": "city"}, "id": "place:4819000", "key": "4819000", "kind": "place", "lat": 32.793333, "lon": -96.766513, "name": "Dallas", "rank": 6.12, "resolver": "place", "state": "TX"} +{"aliases": ["big d", "dallas", "dallas texas", "dallas tx"], "attrs": {"land_km2": 879.8, "pop": 1326087, "type": "city"}, "id": "place:4819000", "key": "4819000", "kind": "place", "lat": 32.793333, "lon": -96.766513, "name": "Dallas", "rank": 6.12, "resolver": "place", "state": "TX"} {"aliases": ["dalworthington gardens", "dalworthington gardens texas", "dalworthington gardens tx"], "attrs": {"land_km2": 4.6, "pop": 2299, "type": "city"}, "id": "place:4819084", "key": "4819084", "kind": "place", "lat": 32.686262, "lon": -97.155115, "name": "Dalworthington Gardens", "rank": 3.36, "resolver": "place", "state": "TX"} {"aliases": ["damon", "damon texas", "damon tx"], "attrs": {"land_km2": 3.6, "pop": 0, "type": "cdp"}, "id": "place:4819108", "key": "4819108", "kind": "place", "lat": 29.283237, "lon": -95.740772, "name": "Damon", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["danbury", "danbury texas", "danbury tx"], "attrs": {"land_km2": 2.5, "pop": 1699, "type": "city"}, "id": "place:4819120", "key": "4819120", "kind": "place", "lat": 29.227417, "lon": -95.34617, "name": "Danbury", "rank": 3.23, "resolver": "place", "state": "TX"} @@ -27434,7 +27434,7 @@ {"aliases": ["fort hancock", "fort hancock texas", "fort hancock tx"], "attrs": {"land_km2": 49.1, "pop": 0, "type": "cdp"}, "id": "place:4826724", "key": "4826724", "kind": "place", "lat": 31.289272, "lon": -105.845064, "name": "Fort Hancock", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["fort hood", "fort hood texas", "fort hood tx"], "attrs": {"land_km2": 50.1, "pop": 0, "type": "cdp"}, "id": "place:4826736", "key": "4826736", "kind": "place", "lat": 31.136873, "lon": -97.784923, "name": "Fort Hood", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["fort stockton", "fort stockton texas", "fort stockton tx"], "attrs": {"land_km2": 14.4, "pop": 8147, "type": "city"}, "id": "place:4826808", "key": "4826808", "kind": "place", "lat": 30.892574, "lon": -102.88421, "name": "Fort Stockton", "rank": 3.91, "resolver": "place", "state": "TX"} -{"aliases": ["fort worth", "fort worth texas", "fort worth tx"], "attrs": {"land_km2": 911.5, "pop": 1008106, "type": "city"}, "id": "place:4827000", "key": "4827000", "kind": "place", "lat": 32.781954, "lon": -97.348573, "name": "Fort Worth", "rank": 6.0, "resolver": "place", "state": "TX"} +{"aliases": ["cowtown", "fort worth", "fort worth texas", "fort worth tx", "funky town", "panther city"], "attrs": {"land_km2": 911.5, "pop": 1008106, "type": "city"}, "id": "place:4827000", "key": "4827000", "kind": "place", "lat": 32.781954, "lon": -97.348573, "name": "Fort Worth", "rank": 6.0, "resolver": "place", "state": "TX"} {"aliases": ["four corners", "four corners texas", "four corners tx"], "attrs": {"land_km2": 6.3, "pop": 0, "type": "cdp"}, "id": "place:4827102", "key": "4827102", "kind": "place", "lat": 29.670331, "lon": -95.660042, "name": "Four Corners", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["four points", "four points texas", "four points tx"], "attrs": {"land_km2": 1.3, "pop": 0, "type": "cdp"}, "id": "place:4827124", "key": "4827124", "kind": "place", "lat": 27.796407, "lon": -99.454823, "name": "Four Points", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["fowlerton", "fowlerton texas", "fowlerton tx"], "attrs": {"land_km2": 5.6, "pop": 0, "type": "cdp"}, "id": "place:4827144", "key": "4827144", "kind": "place", "lat": 28.452766, "lon": -98.809567, "name": "Fowlerton", "rank": 0.0, "resolver": "place", "state": "TX"} @@ -27606,7 +27606,7 @@ {"aliases": ["hornsby bend", "hornsby bend texas", "hornsby bend tx"], "attrs": {"land_km2": 24.3, "pop": 0, "type": "cdp"}, "id": "place:4834856", "key": "4834856", "kind": "place", "lat": 30.239894, "lon": -97.590177, "name": "Hornsby Bend", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["horseshoe bay", "horseshoe bay texas", "horseshoe bay tx"], "attrs": {"land_km2": 37.1, "pop": 5218, "type": "city"}, "id": "place:4834862", "key": "4834862", "kind": "place", "lat": 30.540758, "lon": -98.38057, "name": "Horseshoe Bay", "rank": 3.72, "resolver": "place", "state": "TX"} {"aliases": ["horseshoe bend", "horseshoe bend texas", "horseshoe bend tx"], "attrs": {"land_km2": 4.3, "pop": 0, "type": "cdp"}, "id": "place:4834865", "key": "4834865", "kind": "place", "lat": 32.578992, "lon": -97.88516, "name": "Horseshoe Bend", "rank": 0.0, "resolver": "place", "state": "TX"} -{"aliases": ["houston", "houston texas", "houston tx"], "attrs": {"land_km2": 1659.5, "pop": 2390125, "type": "city"}, "id": "place:4835000", "key": "4835000", "kind": "place", "lat": 29.785743, "lon": -95.388806, "name": "Houston", "rank": 6.38, "resolver": "place", "state": "TX"} +{"aliases": ["h town", "houston", "houston texas", "houston tx", "htown", "space city"], "attrs": {"land_km2": 1659.5, "pop": 2390125, "type": "city"}, "id": "place:4835000", "key": "4835000", "kind": "place", "lat": 29.785743, "lon": -95.388806, "name": "Houston", "rank": 6.38, "resolver": "place", "state": "TX"} {"aliases": ["howardwick", "howardwick texas", "howardwick tx"], "attrs": {"land_km2": 5.2, "pop": 367, "type": "city"}, "id": "place:4835072", "key": "4835072", "kind": "place", "lat": 35.034812, "lon": -100.907847, "name": "Howardwick", "rank": 2.56, "resolver": "place", "state": "TX"} {"aliases": ["howe", "howe texas", "howe tx"], "attrs": {"land_km2": 12.9, "pop": 3667, "type": "town"}, "id": "place:4835084", "key": "4835084", "kind": "place", "lat": 33.507477, "lon": -96.615543, "name": "Howe", "rank": 3.56, "resolver": "place", "state": "TX"} {"aliases": ["hubbard", "hubbard texas", "hubbard tx"], "attrs": {"land_km2": 5.1, "pop": 1480, "type": "city"}, "id": "place:4835180", "key": "4835180", "kind": "place", "lat": 31.846821, "lon": -96.799998, "name": "Hubbard", "rank": 3.17, "resolver": "place", "state": "TX"} @@ -27688,7 +27688,7 @@ {"aliases": ["kerens", "kerens texas", "kerens tx"], "attrs": {"land_km2": 6.5, "pop": 1508, "type": "city"}, "id": "place:4838992", "key": "4838992", "kind": "place", "lat": 32.131157, "lon": -96.2257, "name": "Kerens", "rank": 3.18, "resolver": "place", "state": "TX"} {"aliases": ["kermit", "kermit texas", "kermit tx"], "attrs": {"land_km2": 6.6, "pop": 5909, "type": "city"}, "id": "place:4839004", "key": "4839004", "kind": "place", "lat": 31.853923, "lon": -103.092395, "name": "Kermit", "rank": 3.77, "resolver": "place", "state": "TX"} {"aliases": ["kerrville", "kerrville texas", "kerrville tx"], "attrs": {"land_km2": 59.4, "pop": 25141, "type": "city"}, "id": "place:4839040", "key": "4839040", "kind": "place", "lat": 30.038317, "lon": -99.131069, "name": "Kerrville", "rank": 4.4, "resolver": "place", "state": "TX"} -{"aliases": ["kilgore", "kilgore texas", "kilgore tx"], "attrs": {"land_km2": 48.2, "pop": 13583, "type": "city"}, "id": "place:4839124", "key": "4839124", "kind": "place", "lat": 32.396669, "lon": -94.860013, "name": "Kilgore", "rank": 4.13, "resolver": "place", "state": "TX"} +{"aliases": ["city of stars", "k town", "kilgore", "kilgore texas", "kilgore tx", "ksc"], "attrs": {"land_km2": 48.2, "pop": 13583, "type": "city"}, "id": "place:4839124", "key": "4839124", "kind": "place", "lat": 32.396669, "lon": -94.860013, "name": "Kilgore", "rank": 4.13, "resolver": "place", "state": "TX"} {"aliases": ["killeen", "killeen texas", "killeen tx"], "attrs": {"land_km2": 141.5, "pop": 160616, "type": "city"}, "id": "place:4839148", "key": "4839148", "kind": "place", "lat": 31.077669, "lon": -97.731952, "name": "Killeen", "rank": 5.21, "resolver": "place", "state": "TX"} {"aliases": ["kingsbury", "kingsbury texas", "kingsbury tx"], "attrs": {"land_km2": 6.7, "pop": 143, "type": "city"}, "id": "place:4839292", "key": "4839292", "kind": "place", "lat": 29.65001, "lon": -97.818585, "name": "Kingsbury", "rank": 2.16, "resolver": "place", "state": "TX"} {"aliases": ["kingsland", "kingsland texas", "kingsland tx"], "attrs": {"land_km2": 23.4, "pop": 0, "type": "cdp"}, "id": "place:4839304", "key": "4839304", "kind": "place", "lat": 30.666141, "lon": -98.454026, "name": "Kingsland", "rank": 0.0, "resolver": "place", "state": "TX"} @@ -27774,7 +27774,7 @@ {"aliases": ["la presa", "la presa texas", "la presa tx"], "attrs": {"land_km2": 1.3, "pop": 0, "type": "cdp"}, "id": "place:4841449", "key": "4841449", "kind": "place", "lat": 27.399507, "lon": -99.432496, "name": "La Presa", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["la pryor", "la pryor texas", "la pryor tx"], "attrs": {"land_km2": 6.9, "pop": 0, "type": "cdp"}, "id": "place:4841452", "key": "4841452", "kind": "place", "lat": 28.949057, "lon": -99.847918, "name": "La Pryor", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["la puerta", "la puerta texas", "la puerta tx"], "attrs": {"land_km2": 0.8, "pop": 0, "type": "cdp"}, "id": "place:4841458", "key": "4841458", "kind": "place", "lat": 26.346828, "lon": -98.750552, "name": "La Puerta", "rank": 0.0, "resolver": "place", "state": "TX"} -{"aliases": ["laredo", "laredo texas", "laredo tx"], "attrs": {"land_km2": 282.5, "pop": 261260, "type": "city"}, "id": "place:4841464", "key": "4841464", "kind": "place", "lat": 27.560379, "lon": -99.489181, "name": "Laredo", "rank": 5.42, "resolver": "place", "state": "TX"} +{"aliases": ["gateway city", "laredo", "laredo texas", "laredo tx"], "attrs": {"land_km2": 282.5, "pop": 261260, "type": "city"}, "id": "place:4841464", "key": "4841464", "kind": "place", "lat": 27.560379, "lon": -99.489181, "name": "Laredo", "rank": 5.42, "resolver": "place", "state": "TX"} {"aliases": ["laredo ranchettes", "laredo ranchettes texas", "laredo ranchettes tx"], "attrs": {"land_km2": 1.4, "pop": 0, "type": "cdp"}, "id": "place:4841473", "key": "4841473", "kind": "place", "lat": 27.491353, "lon": -99.359782, "name": "Laredo Ranchettes", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["laredo ranchettes west", "laredo ranchettes west texas", "laredo ranchettes west tx"], "attrs": {"land_km2": 0.2, "pop": 0, "type": "cdp"}, "id": "place:4841475", "key": "4841475", "kind": "place", "lat": 27.490038, "lon": -99.370143, "name": "Laredo Ranchettes West", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["la rosita", "la rosita texas", "la rosita tx"], "attrs": {"land_km2": 0.2, "pop": 0, "type": "cdp"}, "id": "place:4841503", "key": "4841503", "kind": "place", "lat": 26.401682, "lon": -98.9321, "name": "La Rosita", "rank": 0.0, "resolver": "place", "state": "TX"} @@ -27880,7 +27880,7 @@ {"aliases": ["loving", "loving texas", "loving tx"], "attrs": {"land_km2": 5.1, "pop": 0, "type": "cdp"}, "id": "place:4844284", "key": "4844284", "kind": "place", "lat": 33.257815, "lon": -98.521927, "name": "Loving", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["lowry crossing", "lowry crossing texas", "lowry crossing tx"], "attrs": {"land_km2": 6.7, "pop": 2028, "type": "city"}, "id": "place:4844308", "key": "4844308", "kind": "place", "lat": 33.169279, "lon": -96.544985, "name": "Lowry Crossing", "rank": 3.31, "resolver": "place", "state": "TX"} {"aliases": ["lozano", "lozano texas", "lozano tx"], "attrs": {"land_km2": 0.3, "pop": 0, "type": "cdp"}, "id": "place:4844344", "key": "4844344", "kind": "place", "lat": 26.18959, "lon": -97.54373, "name": "Lozano", "rank": 0.0, "resolver": "place", "state": "TX"} -{"aliases": ["lubbock", "lubbock texas", "lubbock tx"], "attrs": {"land_km2": 370.9, "pop": 272086, "type": "city"}, "id": "place:4845000", "key": "4845000", "kind": "place", "lat": 33.561901, "lon": -101.888883, "name": "Lubbock", "rank": 5.43, "resolver": "place", "state": "TX"} +{"aliases": ["hub city", "lubbock", "lubbock texas", "lubbock tx"], "attrs": {"land_km2": 370.9, "pop": 272086, "type": "city"}, "id": "place:4845000", "key": "4845000", "kind": "place", "lat": 33.561901, "lon": -101.888883, "name": "Lubbock", "rank": 5.43, "resolver": "place", "state": "TX"} {"aliases": ["lucas", "lucas texas", "lucas tx"], "attrs": {"land_km2": 40.4, "pop": 8840, "type": "city"}, "id": "place:4845012", "key": "4845012", "kind": "place", "lat": 33.101031, "lon": -96.581182, "name": "Lucas", "rank": 3.95, "resolver": "place", "state": "TX"} {"aliases": ["lueders", "lueders texas", "lueders tx"], "attrs": {"land_km2": 1.5, "pop": 261, "type": "city"}, "id": "place:4845048", "key": "4845048", "kind": "place", "lat": 32.800432, "lon": -99.623217, "name": "Lueders", "rank": 2.42, "resolver": "place", "state": "TX"} {"aliases": ["lufkin", "lufkin texas", "lufkin tx"], "attrs": {"land_km2": 88.6, "pop": 34399, "type": "city"}, "id": "place:4845072", "key": "4845072", "kind": "place", "lat": 31.322904, "lon": -94.729026, "name": "Lufkin", "rank": 4.54, "resolver": "place", "state": "TX"} @@ -28358,7 +28358,7 @@ {"aliases": ["samnorwood", "samnorwood texas", "samnorwood tx"], "attrs": {"land_km2": 4.2, "pop": 0, "type": "cdp"}, "id": "place:4864448", "key": "4864448", "kind": "place", "lat": 35.050114, "lon": -100.281401, "name": "Samnorwood", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["sam rayburn", "sam rayburn texas", "sam rayburn tx"], "attrs": {"land_km2": 21.7, "pop": 0, "type": "cdp"}, "id": "place:4864460", "key": "4864460", "kind": "place", "lat": 31.077263, "lon": -94.025528, "name": "Sam Rayburn", "rank": 0.0, "resolver": "place", "state": "TX"} {"aliases": ["san angelo", "san angelo texas", "san angelo tx"], "attrs": {"land_km2": 155.6, "pop": 100159, "type": "city"}, "id": "place:4864472", "key": "4864472", "kind": "place", "lat": 31.441078, "lon": -100.450499, "name": "San Angelo", "rank": 5.0, "resolver": "place", "state": "TX"} -{"aliases": ["san antonio", "san antonio texas", "san antonio tx"], "attrs": {"land_km2": 1292.3, "pop": 1526656, "type": "city"}, "id": "place:4865000", "key": "4865000", "kind": "place", "lat": 29.462809, "lon": -98.524635, "name": "San Antonio", "rank": 6.18, "resolver": "place", "state": "TX"} +{"aliases": ["san antonio", "san antonio texas", "san antonio tx", "satx"], "attrs": {"land_km2": 1292.3, "pop": 1526656, "type": "city"}, "id": "place:4865000", "key": "4865000", "kind": "place", "lat": 29.462809, "lon": -98.524635, "name": "San Antonio", "rank": 6.18, "resolver": "place", "state": "TX"} {"aliases": ["san augustine", "san augustine texas", "san augustine tx"], "attrs": {"land_km2": 12.2, "pop": 1848, "type": "city"}, "id": "place:4865024", "key": "4865024", "kind": "place", "lat": 31.529899, "lon": -94.110894, "name": "San Augustine", "rank": 3.27, "resolver": "place", "state": "TX"} {"aliases": ["san benito", "san benito texas", "san benito tx"], "attrs": {"land_km2": 40.9, "pop": 24712, "type": "city"}, "id": "place:4865036", "key": "4865036", "kind": "place", "lat": 26.129398, "lon": -97.64405, "name": "San Benito", "rank": 4.39, "resolver": "place", "state": "TX"} {"aliases": ["san carlos", "san carlos texas", "san carlos tx"], "attrs": {"land_km2": 4.7, "pop": 0, "type": "cdp"}, "id": "place:4865048", "key": "4865048", "kind": "place", "lat": 26.295668, "lon": -98.063057, "name": "San Carlos", "rank": 0.0, "resolver": "place", "state": "TX"} @@ -29003,7 +29003,7 @@ {"aliases": ["st george", "st george ut", "st george utah"], "attrs": {"land_km2": 200.5, "pop": 106288, "type": "city"}, "id": "place:4965330", "key": "4965330", "kind": "place", "lat": 37.077006, "lon": -113.576534, "name": "St. George", "rank": 5.03, "resolver": "place", "state": "UT"} {"aliases": ["salem", "salem ut", "salem utah"], "attrs": {"land_km2": 31.3, "pop": 11283, "type": "city"}, "id": "place:4965770", "key": "4965770", "kind": "place", "lat": 40.055295, "lon": -111.667365, "name": "Salem", "rank": 4.05, "resolver": "place", "state": "UT"} {"aliases": ["salina", "salina ut", "salina utah"], "attrs": {"land_km2": 15.2, "pop": 2610, "type": "city"}, "id": "place:4965880", "key": "4965880", "kind": "place", "lat": 38.938839, "lon": -111.865209, "name": "Salina", "rank": 3.42, "resolver": "place", "state": "UT"} -{"aliases": ["salt lake", "salt lake city", "salt lake city ut", "salt lake city utah", "salt lake ut", "salt lake utah"], "attrs": {"land_km2": 287.2, "pop": 217783, "type": "city"}, "id": "place:4967000", "key": "4967000", "kind": "place", "lat": 40.776928, "lon": -111.930991, "name": "Salt Lake City", "rank": 5.34, "resolver": "place", "state": "UT"} +{"aliases": ["salt lake", "salt lake city", "salt lake city ut", "salt lake city utah", "salt lake ut", "salt lake utah", "slc"], "attrs": {"land_km2": 287.2, "pop": 217783, "type": "city"}, "id": "place:4967000", "key": "4967000", "kind": "place", "lat": 40.776928, "lon": -111.930991, "name": "Salt Lake City", "rank": 5.34, "resolver": "place", "state": "UT"} {"aliases": ["samak", "samak ut", "samak utah"], "attrs": {"land_km2": 6.4, "pop": 0, "type": "cdp"}, "id": "place:4967400", "key": "4967400", "kind": "place", "lat": 40.638907, "lon": -111.2413, "name": "Samak", "rank": 0.0, "resolver": "place", "state": "UT"} {"aliases": ["sandy", "sandy ut", "sandy utah"], "attrs": {"land_km2": 62.8, "pop": 92840, "type": "city"}, "id": "place:4967440", "key": "4967440", "kind": "place", "lat": 40.571714, "lon": -111.84987, "name": "Sandy", "rank": 4.97, "resolver": "place", "state": "UT"} {"aliases": ["santa clara", "santa clara ut", "santa clara utah"], "attrs": {"land_km2": 15.7, "pop": 8631, "type": "city"}, "id": "place:4967660", "key": "4967660", "kind": "place", "lat": 37.131398, "lon": -113.656646, "name": "Santa Clara", "rank": 3.94, "resolver": "place", "state": "UT"} @@ -29787,7 +29787,7 @@ {"aliases": ["rhoadesville", "rhoadesville va", "rhoadesville virginia"], "attrs": {"land_km2": 2.7, "pop": 0, "type": "cdp"}, "id": "place:5166800", "key": "5166800", "kind": "place", "lat": 38.270757, "lon": -77.931614, "name": "Rhoadesville", "rank": 0.0, "resolver": "place", "state": "VA"} {"aliases": ["rich creek", "rich creek va", "rich creek virginia"], "attrs": {"land_km2": 2.1, "pop": 734, "type": "town"}, "id": "place:5166896", "key": "5166896", "kind": "place", "lat": 37.384617, "lon": -80.818813, "name": "Rich Creek", "rank": 2.87, "resolver": "place", "state": "VA"} {"aliases": ["richlands", "richlands va", "richlands virginia"], "attrs": {"land_km2": 14.6, "pop": 5074, "type": "town"}, "id": "place:5166928", "key": "5166928", "kind": "place", "lat": 37.088022, "lon": -81.810416, "name": "Richlands", "rank": 3.71, "resolver": "place", "state": "VA"} -{"aliases": ["richmond", "richmond va", "richmond virginia"], "attrs": {"land_km2": 155.2, "pop": 233655, "type": "city"}, "id": "place:5167000", "key": "5167000", "kind": "place", "lat": 37.531399, "lon": -77.476009, "name": "Richmond", "rank": 5.37, "resolver": "place", "state": "VA"} +{"aliases": ["richmond", "richmond va", "richmond virginia", "rva"], "attrs": {"land_km2": 155.2, "pop": 233655, "type": "city"}, "id": "place:5167000", "key": "5167000", "kind": "place", "lat": 37.531399, "lon": -77.476009, "name": "Richmond", "rank": 5.37, "resolver": "place", "state": "VA"} {"aliases": ["ridgeway", "ridgeway va", "ridgeway virginia"], "attrs": {"land_km2": 2.5, "pop": 733, "type": "town"}, "id": "place:5167208", "key": "5167208", "kind": "place", "lat": 36.578174, "lon": -79.85829, "name": "Ridgeway", "rank": 2.87, "resolver": "place", "state": "VA"} {"aliases": ["riner", "riner va", "riner virginia"], "attrs": {"land_km2": 3.8, "pop": 0, "type": "cdp"}, "id": "place:5167256", "key": "5167256", "kind": "place", "lat": 37.067043, "lon": -80.440573, "name": "Riner", "rank": 0.0, "resolver": "place", "state": "VA"} {"aliases": ["rio", "rio va", "rio virginia"], "attrs": {"land_km2": 1.6, "pop": 0, "type": "cdp"}, "id": "place:5167288", "key": "5167288", "kind": "place", "lat": 38.080488, "lon": -78.473191, "name": "Rio", "rank": 0.0, "resolver": "place", "state": "VA"} @@ -29908,7 +29908,7 @@ {"aliases": ["villa heights", "villa heights va", "villa heights virginia"], "attrs": {"land_km2": 2.5, "pop": 0, "type": "cdp"}, "id": "place:5181152", "key": "5181152", "kind": "place", "lat": 36.702171, "lon": -79.894608, "name": "Villa Heights", "rank": 0.0, "resolver": "place", "state": "VA"} {"aliases": ["vinton", "vinton va", "vinton virginia"], "attrs": {"land_km2": 8.1, "pop": 8059, "type": "town"}, "id": "place:5181280", "key": "5181280", "kind": "place", "lat": 37.274236, "lon": -79.888194, "name": "Vinton", "rank": 3.91, "resolver": "place", "state": "VA"} {"aliases": ["virgilina", "virgilina va", "virgilina virginia"], "attrs": {"land_km2": 1.6, "pop": 119, "type": "town"}, "id": "place:5181312", "key": "5181312", "kind": "place", "lat": 36.545675, "lon": -78.775213, "name": "Virgilina", "rank": 2.08, "resolver": "place", "state": "VA"} -{"aliases": ["virginia beach", "virginia beach va", "virginia beach virginia"], "attrs": {"land_km2": 633.8, "pop": 454808, "type": "city"}, "id": "place:5182000", "key": "5182000", "kind": "place", "lat": 36.779525, "lon": -76.029142, "name": "Virginia Beach", "rank": 5.66, "resolver": "place", "state": "VA"} +{"aliases": ["resort city", "virginia beach", "virginia beach va", "virginia beach virginia"], "attrs": {"land_km2": 633.8, "pop": 454808, "type": "city"}, "id": "place:5182000", "key": "5182000", "kind": "place", "lat": 36.779525, "lon": -76.029142, "name": "Virginia Beach", "rank": 5.66, "resolver": "place", "state": "VA"} {"aliases": ["wachapreague", "wachapreague va", "wachapreague virginia"], "attrs": {"land_km2": 0.6, "pop": 258, "type": "town"}, "id": "place:5182320", "key": "5182320", "kind": "place", "lat": 37.606415, "lon": -75.689273, "name": "Wachapreague", "rank": 2.41, "resolver": "place", "state": "VA"} {"aliases": ["wakefield", "wakefield va", "wakefield virginia"], "attrs": {"land_km2": 9.8, "pop": 0, "type": "cdp"}, "id": "place:5182371", "key": "5182371", "kind": "place", "lat": 38.824691, "lon": -77.242953, "name": "Wakefield", "rank": 0.0, "resolver": "place", "state": "VA"} {"aliases": ["wakefield", "wakefield va", "wakefield virginia"], "attrs": {"land_km2": 3.2, "pop": 747, "type": "town"}, "id": "place:5182384", "key": "5182384", "kind": "place", "lat": 36.969126, "lon": -76.988096, "name": "Wakefield", "rank": 2.87, "resolver": "place", "state": "VA"} @@ -29916,7 +29916,7 @@ {"aliases": ["warm springs", "warm springs va", "warm springs virginia"], "attrs": {"land_km2": 3.9, "pop": 0, "type": "cdp"}, "id": "place:5183088", "key": "5183088", "kind": "place", "lat": 38.045038, "lon": -79.778713, "name": "Warm Springs", "rank": 0.0, "resolver": "place", "state": "VA"} {"aliases": ["warrenton", "warrenton va", "warrenton virginia"], "attrs": {"land_km2": 11.3, "pop": 10224, "type": "town"}, "id": "place:5183136", "key": "5183136", "kind": "place", "lat": 38.717817, "lon": -77.797316, "name": "Warrenton", "rank": 4.01, "resolver": "place", "state": "VA"} {"aliases": ["warsaw", "warsaw va", "warsaw virginia"], "attrs": {"land_km2": 7.8, "pop": 1683, "type": "town"}, "id": "place:5183168", "key": "5183168", "kind": "place", "lat": 37.960351, "lon": -76.76095, "name": "Warsaw", "rank": 3.23, "resolver": "place", "state": "VA"} -{"aliases": ["washington", "washington va", "washington virginia"], "attrs": {"land_km2": 0.7, "pop": 84, "type": "town"}, "id": "place:5183248", "key": "5183248", "kind": "place", "lat": 38.711733, "lon": -78.159959, "name": "Washington", "rank": 1.92, "resolver": "place", "state": "VA"} +{"aliases": ["little washington", "washington", "washington va", "washington virginia"], "attrs": {"land_km2": 0.7, "pop": 84, "type": "town"}, "id": "place:5183248", "key": "5183248", "kind": "place", "lat": 38.711733, "lon": -78.159959, "name": "Washington", "rank": 1.92, "resolver": "place", "state": "VA"} {"aliases": ["waterford", "waterford va", "waterford virginia"], "attrs": {"land_km2": 0.9, "pop": 0, "type": "cdp"}, "id": "place:5183440", "key": "5183440", "kind": "place", "lat": 39.185174, "lon": -77.611896, "name": "Waterford", "rank": 0.0, "resolver": "place", "state": "VA"} {"aliases": ["wattsville", "wattsville va", "wattsville virginia"], "attrs": {"land_km2": 11.1, "pop": 0, "type": "cdp"}, "id": "place:5183568", "key": "5183568", "kind": "place", "lat": 37.929099, "lon": -75.492534, "name": "Wattsville", "rank": 0.0, "resolver": "place", "state": "VA"} {"aliases": ["waverly", "waverly va", "waverly virginia"], "attrs": {"land_km2": 8.0, "pop": 1934, "type": "town"}, "id": "place:5183600", "key": "5183600", "kind": "place", "lat": 37.034024, "lon": -77.095596, "name": "Waverly", "rank": 3.29, "resolver": "place", "state": "VA"} @@ -30455,7 +30455,7 @@ {"aliases": ["schwana", "schwana wa", "schwana washington"], "attrs": {"land_km2": 0.6, "pop": 0, "type": "cdp"}, "id": "place:5361925", "key": "5361925", "kind": "place", "lat": 46.822086, "lon": -119.923336, "name": "Schwana", "rank": 0.0, "resolver": "place", "state": "WA"} {"aliases": ["seabeck", "seabeck wa", "seabeck washington"], "attrs": {"land_km2": 8.6, "pop": 0, "type": "cdp"}, "id": "place:5362120", "key": "5362120", "kind": "place", "lat": 47.644263, "lon": -122.817973, "name": "Seabeck", "rank": 0.0, "resolver": "place", "state": "WA"} {"aliases": ["seatac", "seatac wa", "seatac washington"], "attrs": {"land_km2": 26.1, "pop": 34024, "type": "city"}, "id": "place:5362288", "key": "5362288", "kind": "place", "lat": 47.443403, "lon": -122.298287, "name": "SeaTac", "rank": 4.53, "resolver": "place", "state": "WA"} -{"aliases": ["seattle", "seattle wa", "seattle washington"], "attrs": {"land_km2": 217.6, "pop": 780995, "type": "city"}, "id": "place:5363000", "key": "5363000", "kind": "place", "lat": 47.619335, "lon": -122.351538, "name": "Seattle", "rank": 5.89, "resolver": "place", "state": "WA"} +{"aliases": ["emerald city", "jet city", "seattle", "seattle wa", "seattle washington"], "attrs": {"land_km2": 217.6, "pop": 780995, "type": "city"}, "id": "place:5363000", "key": "5363000", "kind": "place", "lat": 47.619335, "lon": -122.351538, "name": "Seattle", "rank": 5.89, "resolver": "place", "state": "WA"} {"aliases": ["sedro", "sedro wa", "sedro washington", "sedro woolley", "sedro woolley wa", "sedro woolley washington"], "attrs": {"land_km2": 11.3, "pop": 13215, "type": "city"}, "id": "place:5363210", "key": "5363210", "kind": "place", "lat": 48.511356, "lon": -122.232137, "name": "Sedro-Woolley", "rank": 4.12, "resolver": "place", "state": "WA"} {"aliases": ["sekiu", "sekiu wa", "sekiu washington"], "attrs": {"land_km2": 0.3, "pop": 0, "type": "cdp"}, "id": "place:5363245", "key": "5363245", "kind": "place", "lat": 48.262998, "lon": -124.303469, "name": "Sekiu", "rank": 0.0, "resolver": "place", "state": "WA"} {"aliases": ["selah", "selah wa", "selah washington"], "attrs": {"land_km2": 11.9, "pop": 8653, "type": "city"}, "id": "place:5363280", "key": "5363280", "kind": "place", "lat": 46.645856, "lon": -120.538257, "name": "Selah", "rank": 3.94, "resolver": "place", "state": "WA"} @@ -31496,7 +31496,7 @@ {"aliases": ["millston", "millston wi", "millston wisconsin"], "attrs": {"land_km2": 2.7, "pop": 0, "type": "cdp"}, "id": "place:5552025", "key": "5552025", "kind": "place", "lat": 44.189264, "lon": -90.645691, "name": "Millston", "rank": 0.0, "resolver": "place", "state": "WI"} {"aliases": ["milltown", "milltown wi", "milltown wisconsin"], "attrs": {"land_km2": 4.7, "pop": 938, "type": "village"}, "id": "place:5552075", "key": "5552075", "kind": "place", "lat": 45.521082, "lon": -92.498503, "name": "Milltown", "rank": 2.97, "resolver": "place", "state": "WI"} {"aliases": ["milton", "milton wi", "milton wisconsin"], "attrs": {"land_km2": 12.2, "pop": 5778, "type": "city"}, "id": "place:5552200", "key": "5552200", "kind": "place", "lat": 42.774675, "lon": -88.939991, "name": "Milton", "rank": 3.76, "resolver": "place", "state": "WI"} -{"aliases": ["milwaukee", "milwaukee wi", "milwaukee wisconsin"], "attrs": {"land_km2": 249.1, "pop": 563531, "type": "city"}, "id": "place:5553000", "key": "5553000", "kind": "place", "lat": 43.063348, "lon": -87.966695, "name": "Milwaukee", "rank": 5.75, "resolver": "place", "state": "WI"} +{"aliases": ["brew city", "cream city", "mil town", "milwaukee", "milwaukee wi", "milwaukee wisconsin"], "attrs": {"land_km2": 249.1, "pop": 563531, "type": "city"}, "id": "place:5553000", "key": "5553000", "kind": "place", "lat": 43.063348, "lon": -87.966695, "name": "Milwaukee", "rank": 5.75, "resolver": "place", "state": "WI"} {"aliases": ["mindoro", "mindoro wi", "mindoro wisconsin"], "attrs": {"land_km2": 1.6, "pop": 0, "type": "cdp"}, "id": "place:5553050", "key": "5553050", "kind": "place", "lat": 44.020139, "lon": -91.101926, "name": "Mindoro", "rank": 0.0, "resolver": "place", "state": "WI"} {"aliases": ["mineral point", "mineral point wi", "mineral point wisconsin"], "attrs": {"land_km2": 8.1, "pop": 2560, "type": "city"}, "id": "place:5553100", "key": "5553100", "kind": "place", "lat": 42.863223, "lon": -90.181793, "name": "Mineral Point", "rank": 3.41, "resolver": "place", "state": "WI"} {"aliases": ["minocqua", "minocqua wi", "minocqua wisconsin"], "attrs": {"land_km2": 1.9, "pop": 0, "type": "cdp"}, "id": "place:5553200", "key": "5553200", "kind": "place", "lat": 45.879211, "lon": -89.703617, "name": "Minocqua", "rank": 0.0, "resolver": "place", "state": "WI"} diff --git a/resolvers/place_nicknames.json b/resolvers/place_nicknames.json new file mode 100644 index 0000000..dcca029 --- /dev/null +++ b/resolvers/place_nicknames.json @@ -0,0 +1,38 @@ +{ + "description": "Reviewed nicknames and short names for US places, added as aliases by `sourcetool resolvers place` on top of Wikidata's (P1813 short name, P1449 nickname). Only names people commonly type, and only where the city is what nearly everyone means. A two-letter nickname counts only when typed in capitals (LA, SF, DC, KC); see docs/RESOLVERS.md. `exclude` lists Wikidata nicknames that are not added (derogatory or misleading).", + "exclude": ["Bodymore, Murderland"], + "entries": [ + {"place": "New York", "state": "NY", "nicknames": ["NYC", "New York City", "Big Apple"]}, + {"place": "Los Angeles", "state": "CA", "nicknames": ["LA", "L.A."]}, + {"place": "San Francisco", "state": "CA", "nicknames": ["SF", "San Fran"]}, + {"place": "Philadelphia", "state": "PA", "nicknames": ["Philly"]}, + {"place": "Las Vegas", "state": "NV", "nicknames": ["Vegas"]}, + {"place": "Washington", "state": "DC", "nicknames": ["DC", "D.C."]}, + {"place": "New Orleans", "state": "LA", "nicknames": ["NOLA", "Big Easy"]}, + {"place": "Chicago", "state": "IL", "nicknames": ["Chi-town", "Chitown", "Windy City"]}, + {"place": "Houston", "state": "TX", "nicknames": ["H-Town", "Htown"]}, + {"place": "Atlanta", "state": "GA", "nicknames": ["ATL"]}, + {"place": "Detroit", "state": "MI", "nicknames": ["Motor City", "Motown"]}, + {"place": "Nashville", "state": "TN", "nicknames": ["Music City", "Nashvegas"]}, + {"place": "Indianapolis", "state": "IN", "nicknames": ["Indy"]}, + {"place": "Cincinnati", "state": "OH", "nicknames": ["Cincy"]}, + {"place": "Columbus", "state": "OH", "nicknames": ["Cbus"]}, + {"place": "Jacksonville", "state": "FL", "nicknames": ["Jax"]}, + {"place": "Oklahoma City", "state": "OK", "nicknames": ["OKC"]}, + {"place": "Salt Lake City", "state": "UT", "nicknames": ["SLC"]}, + {"place": "Kansas City", "state": "MO", "nicknames": ["KC"]}, + {"place": "St. Louis", "state": "MO", "nicknames": ["STL"]}, + {"place": "Albuquerque", "state": "NM", "nicknames": ["ABQ"]}, + {"place": "Minneapolis", "state": "MN", "nicknames": ["Mpls"]}, + {"place": "Pittsburgh", "state": "PA", "nicknames": ["Steel City", "PGH"]}, + {"place": "Baltimore", "state": "MD", "nicknames": ["Bmore", "Charm City"]}, + {"place": "San Antonio", "state": "TX", "nicknames": ["SATX"]}, + {"place": "Phoenix", "state": "AZ", "nicknames": ["PHX"]}, + {"place": "Portland", "state": "OR", "nicknames": ["PDX"]}, + {"place": "Austin", "state": "TX", "nicknames": ["ATX"]}, + {"place": "Charlotte", "state": "NC", "nicknames": ["CLT"]}, + {"place": "Sacramento", "state": "CA", "nicknames": ["Sactown"]}, + {"place": "St. Petersburg", "state": "FL", "nicknames": ["St. Pete"]}, + {"place": "Fort Lauderdale", "state": "FL", "nicknames": ["Ft. Lauderdale"]} + ] +} diff --git a/service/README.md b/service/README.md new file mode 100644 index 0000000..3a814e7 --- /dev/null +++ b/service/README.md @@ -0,0 +1,109 @@ +# SmartBrain Library API + +The small service the SmartBrain app talks to instead of Git (operator ruling R7, `docs/PLAN.md` section 4). +It will run on the VPS behind the existing Caddy at `https://smartbrain.securecloudgroup.com/library/v1/`. + +| Endpoint | Who | What | +|---|---|---| +| `POST /library/v1/votes` | the app | `{"source_id", "verdict": "yes"\|"no"\|"broken", "app_version"}` for a Library source | +| `POST /library/v1/suggestions` | the app | `{"record", "via": "form"\|"yes", "app_version"}`: a new source, parameter values removed | +| `GET /library/v1/manifest` | the app | the current pack `{tag, url, sha256, bytes}`, from the operator's manifest file | +| `GET /library/v1/healthz` | anyone | `{"ok": true}` | +| `GET /library/v1/admin/queue?since=&limit=` | operator | queued items after `since` (bearer token) | +| `POST /library/v1/admin/ack` | operator | `{"through": id}` deletes the queue up to that id (bearer token) | + +Accepted submissions answer `202`. Refusals answer `422` with a list of problems, `413` (body over 16 KB), +`415` (not JSON), `429` with `Retry-After` (rate limit) or `503` (queue full). + +## What it checks +- Every suggestion runs through the same `validate_record` rules as CI (`sourcetool/schema.py`), plus + `sourcetool/submission.py`: https only, a public host (no private or loopback IPs, no `.local`), no + credential in the address, headers or userinfo, **no parameter values** (`example` must be null), known + categories, kinds and parameter kinds, only known fields, and size caps on every text and list. +- Nothing the client says about trust is kept. The tier becomes `harvested`, votes start at zero, + validation is `unvalidated`, the terms status is `unverified`, the provider's authority is `community`, + the origin is `user-suggestion`, and the id is derived from the host and name (never the client's id). +- The service **never fetches** a submitted URL. `sourcetool ingest` re-checks and probes on the + operator's side. + +## Privacy and security +- No accounts, cookies, install ids or device ids. Request models reject any field they do not name. +- The queue stores only what the app sends (a source id and verdict, or a record with parameter values + removed), the app version, and the **day** it arrived. Never the ask text, a location, a key, or an + identifier. +- Client addresses exist only in the in-memory rate limiter (30 votes and 10 suggestions per hour per + address); they are never written to disk. Uvicorn runs with `--no-access-log`. Request bodies are never + logged. Error messages name the field that is wrong and do not echo values back. +- Behind Caddy, the address is the single `X-Forwarded-For` entry Caddy sets, and it is believed only + when the request comes from `LIBRARY_TRUSTED_PROXIES`. +- No CORS headers (the desktop app calls the API from its server side), JSON-only POSTs (a browser + cannot send a cross-site simple form post), `nosniff`, `no-store`, `DENY` framing and a `default-src + 'none'` CSP on every response. No docs or OpenAPI endpoints. +- The admin endpoints need `Authorization: Bearer ` (compared in constant time). A + token shorter than 32 characters, or none, disables them. +- The queue holds at most 100,000 items; beyond that submissions get `503` until the operator ingests. + +## Run the tests +```bash +python3.12 -m venv .venv-service && .venv-service/bin/pip install -r service/requirements-test.txt +.venv-service/bin/python -m pytest service/tests tests/test_ingest.py +``` + +## Deploy on the VPS (operator) +The VPS's Caddy runs in the `sb_caddy` container, so the service listens on the host's `docker0` +address (not `127.0.0.1`, which the container cannot reach, and never a public address). Python 3.12 is +required (`python3 --version`). + +On the VPS (`ssh sb-signaling`), as a sudoer: + +```bash +# 1. a user with no login and no home, and the code (read-only to that user) +sudo useradd --system --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin sblibrary +sudo git clone https://github.com/SecureCloudGroup/SmartBrain_Library /opt/smartbrain-library +sudo python3 -m venv /opt/smartbrain-library/.venv +sudo /opt/smartbrain-library/.venv/bin/pip install -r /opt/smartbrain-library/service/requirements.txt + +# 2. configuration: the token and the manifest +ip -4 addr show docker0 # expect 172.17.0.1; if not, use that address below and in library.caddy +sudo install -d -m 750 -o root -g sblibrary /etc/smartbrain-library +sudo install -m 640 -o root -g sblibrary /opt/smartbrain-library/service/deploy/env.example /etc/smartbrain-library/env +TOKEN=$(openssl rand -hex 32); echo "$TOKEN" # keep it; the Mac needs it for `sourcetool ingest` +sudo sed -i "s/^LIBRARY_ADMIN_TOKEN=.*/LIBRARY_ADMIN_TOKEN=$TOKEN/" /etc/smartbrain-library/env +TAG=v1.1.0; BASE=https://github.com/SecureCloudGroup/SmartBrain_Library/releases/download/$TAG +SHA=$(curl -fsSL $BASE/library.duckdb.gz.sha256 | cut -d' ' -f1) +BYTES=$(curl -fsSL $BASE/library.duckdb.gz | wc -c) +printf '{"tag": "%s", "url": "%s/library.duckdb.gz", "sha256": "%s", "bytes": %s}\n' "$TAG" "$BASE" "$SHA" "$BYTES" \ + | sudo install -m 644 /dev/stdin /etc/smartbrain-library/manifest.json + +# 3. the service +sudo cp /opt/smartbrain-library/service/deploy/smartbrain-library.service /etc/systemd/system/ +sudo systemctl daemon-reload && sudo systemctl enable --now smartbrain-library +systemctl status smartbrain-library --no-pager +curl -s http://172.17.0.1:8095/library/v1/healthz # {"ok":true} + +# 4. Caddy: paste service/deploy/library.caddy INSIDE the {$LANDING_DOMAIN} { ... } block, above file_server +nano ~/sb-node/compose/conf.d/landing.caddy +docker exec sb_caddy caddy validate --config /etc/caddy/Caddyfile +docker exec sb_caddy caddy reload --config /etc/caddy/Caddyfile +curl -s https://smartbrain.securecloudgroup.com/library/v1/healthz # {"ok":true} +curl -s https://smartbrain.securecloudgroup.com/library/v1/manifest # the pack +curl -s -o /dev/null -w '%{http_code}\n' https://smartbrain.securecloudgroup.com/library/v1/admin/queue # 401 +``` + +Make the same `landing.caddy` change in SmartBrain_3000's `compose/conf.d/landing.caddy`, so a future +redeploy of the node from that repo keeps the route. + +**Update** the service: `cd /opt/smartbrain-library && sudo git pull --ff-only && sudo .venv/bin/pip install +-r service/requirements.txt && sudo systemctl restart smartbrain-library`. +**New pack**: rewrite `/etc/smartbrain-library/manifest.json` (step 2); it is read on every request. +**Backup**: `/var/lib/smartbrain-library/queue.sqlite3` holds only what has not been ingested yet. + +## Ingest (the operator's machine) +```bash +export LIBRARY_ADMIN_TOKEN= # LIBRARY_API_URL defaults to the VPS +.venv/bin/python -m sourcetool ingest --dry-run # what is queued; writes nothing, clears nothing +.venv/bin/python -m sourcetool ingest # write votes + sources/suggested/.jsonl, probe, clear the queue +.venv/bin/python -m sourcetool ingest --commit # the same, then a local branch + commit (never pushes) +``` +Then review the diff, run `check`, and open the PR. The queue is cleared once the files are written, so +keep the branch until it is merged. diff --git a/service/__init__.py b/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/app.py b/service/app.py new file mode 100644 index 0000000..695aaca --- /dev/null +++ b/service/app.py @@ -0,0 +1,305 @@ +"""The SmartBrain Library API (docs/PLAN.md section 4, operator ruling R7): the app submits votes and +suggestions here instead of through Git; the operator's `sourcetool ingest` pulls the queue. + +Run from the repository root, so the service checks records with the same `sourcetool` rules as CI: + + uvicorn --factory service.app:create_app --host 127.0.0.1 --port 8095 --no-access-log --no-server-header + +Privacy (docs/POLICY.md): no accounts, cookies or install ids. The queue stores only what the app +sends (a source id and verdict, or a source record with parameter values removed), the app version and +the day. Client IPs live only in the in-memory rate limiter and are never written or logged. The +service never fetches a submitted URL. + +Configuration (environment): + LIBRARY_DB SQLite queue file (default: service/data/queue.sqlite3) + LIBRARY_MANIFEST JSON file with the current pack: {"tag", "url", "sha256", "bytes"} + LIBRARY_ADMIN_TOKEN bearer token for /admin (at least 32 characters; unset = admin disabled) + LIBRARY_TRUSTED_PROXIES comma-separated proxy addresses or networks whose X-Forwarded-For is + believed (default 127.0.0.1; behind the VPS's Caddy container: 172.16.0.0/12) +""" +from __future__ import annotations + +import hmac +import ipaddress +import json +import math +import os +import re +import sqlite3 +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal + +from fastapi import FastAPI, Query, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from pydantic import BaseModel, ConfigDict, Field + +from sourcetool.submission import APP_VERSION, clean_suggestion, vote_problems + +PREFIX = "/library/v1" +MAX_BODY = 16 * 1024 +MAX_QUEUE = 100_000 # rows waiting for ingest; beyond this the service refuses new ones (503) +SECURITY_HEADERS = [ + (b"x-content-type-options", b"nosniff"), + (b"cache-control", b"no-store"), + (b"referrer-policy", b"no-referrer"), + (b"x-frame-options", b"DENY"), + (b"content-security-policy", b"default-src 'none'; frame-ancestors 'none'"), + (b"cross-origin-resource-policy", b"same-origin"), +] +_SHA256 = re.compile(r"^[0-9a-f]{64}$") + + +@dataclass +class Settings: + db: Path + manifest: Path | None = None + admin_token: str = "" + trusted_proxies: frozenset[str] = frozenset({"127.0.0.1"}) + votes_per_hour: int = 30 + suggestions_per_hour: int = 10 + max_queue: int = MAX_QUEUE + + @classmethod + def from_env(cls) -> Settings: + env = os.environ + return cls(db=Path(env.get("LIBRARY_DB") or Path(__file__).parent / "data" / "queue.sqlite3"), + manifest=Path(env["LIBRARY_MANIFEST"]) if env.get("LIBRARY_MANIFEST") else None, + admin_token=env.get("LIBRARY_ADMIN_TOKEN", ""), + trusted_proxies=frozenset(p.strip() for p in env.get("LIBRARY_TRUSTED_PROXIES", "127.0.0.1") + .split(",") if p.strip())) + + +# ------------------------------------------------------------------------------------------ request models +_VERSION = APP_VERSION.pattern + + +class Vote(BaseModel): + model_config = ConfigDict(extra="forbid", strict=True) + source_id: str = Field(min_length=2, max_length=120, pattern=r"^[a-z0-9][a-z0-9-]{1,119}$") + verdict: Literal["yes", "no", "broken"] + app_version: str = Field(max_length=32, pattern=_VERSION) + + +class Suggestion(BaseModel): + model_config = ConfigDict(extra="forbid", strict=True) + record: dict[str, Any] + via: Literal["form", "yes"] + app_version: str = Field(max_length=32, pattern=_VERSION) + + +class Ack(BaseModel): + model_config = ConfigDict(extra="forbid", strict=True) + through: int = Field(ge=0) + + +# ------------------------------------------------------------------------------------------ pieces +class RateLimiter: + """Per-client token buckets, in memory only (a restart forgets every address).""" + + def __init__(self) -> None: + self._b: dict[tuple[str, str], tuple[float, float]] = {} + self._lock = threading.Lock() + + def take(self, client: str, bucket: str, per_hour: int) -> float: + """0 when allowed; otherwise the seconds until one more request is allowed.""" + rate = per_hour / 3600.0 + now = time.monotonic() + with self._lock: + if len(self._b) > 50_000: # forget clients whose bucket has refilled + self._b = {k: v for k, v in self._b.items() if v[0] + (now - v[1]) * rate < per_hour} + tokens, at = self._b.get((client, bucket), (float(per_hour), now)) + tokens = min(float(per_hour), tokens + (now - at) * rate) + if tokens < 1.0: + self._b[(client, bucket)] = (tokens, now) + return (1.0 - tokens) / rate + self._b[(client, bucket)] = (tokens - 1.0, now) + return 0.0 + + +class Queue: + def __init__(self, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + self._db = sqlite3.connect(str(path), check_same_thread=False, isolation_level=None) + self._db.execute("PRAGMA journal_mode=WAL") + self._db.execute("CREATE TABLE IF NOT EXISTS queue(id INTEGER PRIMARY KEY AUTOINCREMENT, " + "kind TEXT NOT NULL, day TEXT NOT NULL, payload TEXT NOT NULL)") + self._lock = threading.Lock() + + def add(self, kind: str, payload: dict, limit: int) -> bool: + with self._lock: + if self._db.execute("SELECT count(*) FROM queue").fetchone()[0] >= limit: + return False + # the day only: no time of day that could be matched against anything else + self._db.execute("INSERT INTO queue(kind, day, payload) VALUES (?, ?, ?)", + (kind, time.strftime("%Y-%m-%d", time.gmtime()), json.dumps(payload, sort_keys=True))) + return True + + def items(self, since: int, limit: int) -> list[dict]: + with self._lock: + rows = self._db.execute("SELECT id, kind, day, payload FROM queue WHERE id > ? ORDER BY id LIMIT ?", + (since, limit)).fetchall() + return [{"id": i, "kind": k, "day": d, "payload": json.loads(p)} for i, k, d, p in rows] + + def ack(self, through: int) -> int: + with self._lock: + return self._db.execute("DELETE FROM queue WHERE id <= ?", (through,)).rowcount + + def ok(self) -> bool: + with self._lock: + return self._db.execute("SELECT 1").fetchone() == (1,) + + +def client_address(scope: dict, trusted: list) -> str: + """The caller's address: the peer, or the rightmost X-Forwarded-For entry when the peer is our proxy + (Caddy sets that entry to the address it saw, so a client cannot choose it).""" + peer = (scope.get("client") or ("unknown", 0))[0] + try: + ours = any(ipaddress.ip_address(peer) in net for net in trusted) + except ValueError: + ours = False + if ours: + for k, v in scope.get("headers", []): + if k == b"x-forwarded-for": + last = v.decode("latin-1").split(",")[-1].strip() + if last: + return last + return peer + + +class Guard: + """Pure ASGI: body cap, JSON-only POSTs, security headers on every response.""" + + def __init__(self, app, cap: int = MAX_BODY) -> None: + self.app, self.cap = app, cap + + @staticmethod + async def _reply(send, status: int, error: str) -> None: + body = json.dumps({"error": error}).encode() + await send({"type": "http.response.start", "status": status, + "headers": [(b"content-type", b"application/json"), + (b"content-length", str(len(body)).encode()), *SECURITY_HEADERS]}) + await send({"type": "http.response.body", "body": body}) + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + return await self.app(scope, receive, send) + headers = dict(scope.get("headers") or []) + length = headers.get(b"content-length") + if length is not None and (not length.isdigit() or int(length) > self.cap): + return await self._reply(send, 413, "request body too large") + if scope["method"] == "POST" and \ + headers.get(b"content-type", b"").split(b";")[0].strip().lower() != b"application/json": + return await self._reply(send, 415, "send application/json") + body, more = b"", True + while more: + msg = await receive() + if msg["type"] == "http.disconnect": + return + body += msg.get("body", b"") + more = msg.get("more_body", False) + if len(body) > self.cap: + return await self._reply(send, 413, "request body too large") + replayed = False + + async def replay(): + nonlocal replayed + if not replayed: + replayed = True + return {"type": "http.request", "body": body, "more_body": False} + return await receive() + + async def send_with_headers(msg): + if msg["type"] == "http.response.start": + msg = {**msg, "headers": [*msg.get("headers", []), *SECURITY_HEADERS]} + await send(msg) + + await self.app(scope, replay, send_with_headers) + + +# ------------------------------------------------------------------------------------------ the app +def create_app(settings: Settings | None = None) -> Guard: + s = settings or Settings.from_env() + queue = Queue(s.db) + limiter = RateLimiter() + proxies = [ipaddress.ip_network(p, strict=False) for p in s.trusted_proxies] + api = FastAPI(docs_url=None, redoc_url=None, openapi_url=None) + + def _limited(request: Request, bucket: str, per_hour: int) -> JSONResponse | None: + wait = limiter.take(client_address(request.scope, proxies), bucket, per_hour) + if wait: + return JSONResponse({"error": "too many requests"}, status_code=429, + headers={"Retry-After": str(max(1, math.ceil(wait)))}) + return None + + def _admin(request: Request) -> JSONResponse | None: + given = request.headers.get("authorization", "") + want = f"Bearer {s.admin_token}" + if len(s.admin_token) < 32 or not hmac.compare_digest(given.encode(), want.encode()): + return JSONResponse({"error": "unauthorized"}, status_code=401, + headers={"WWW-Authenticate": "Bearer"}) + return None + + @api.exception_handler(RequestValidationError) + async def _invalid(_request, exc: RequestValidationError): + # names what is wrong, never echoes the submitted values + problems = [f"{'.'.join(str(x) for x in e['loc'][1:]) or 'body'}: {e['msg']}" for e in exc.errors()] + return JSONResponse({"error": "invalid request", "problems": problems[:10]}, status_code=422) + + @api.get(f"{PREFIX}/healthz") + def healthz(): + return {"ok": queue.ok()} + + @api.get(f"{PREFIX}/manifest") + def manifest(): + try: + m = json.loads(s.manifest.read_text()) if s.manifest else None + except (OSError, ValueError): + m = None + good = (isinstance(m, dict) and isinstance(m.get("tag"), str) and isinstance(m.get("url"), str) + and m["url"].startswith("https://") and isinstance(m.get("sha256"), str) + and _SHA256.match(m["sha256"]) and isinstance(m.get("bytes"), int) and m["bytes"] > 0) + if not good: + return JSONResponse({"error": "no pack published"}, status_code=503) + return {k: m[k] for k in ("tag", "url", "sha256", "bytes")} + + @api.post(f"{PREFIX}/votes", status_code=202) + def votes(vote: Vote, request: Request): + if (r := _limited(request, "votes", s.votes_per_hour)) is not None: + return r + problems = vote_problems(vote.source_id, vote.verdict) + if problems: + return JSONResponse({"error": "rejected", "problems": problems}, status_code=422) + if not queue.add("vote", vote.model_dump(), s.max_queue): + return JSONResponse({"error": "busy, try later"}, status_code=503, headers={"Retry-After": "3600"}) + return {"accepted": True} + + @api.post(f"{PREFIX}/suggestions", status_code=202) + def suggestions(sug: Suggestion, request: Request): + if (r := _limited(request, "suggestions", s.suggestions_per_hour)) is not None: + return r + record, problems = clean_suggestion(sug.record) + if problems: + return JSONResponse({"error": "rejected", "problems": problems[:20]}, status_code=422) + if not queue.add("suggestion", {"record": record, "via": sug.via, "app_version": sug.app_version}, + s.max_queue): + return JSONResponse({"error": "busy, try later"}, status_code=503, headers={"Retry-After": "3600"}) + return {"accepted": True, "id": record["id"]} + + @api.get(f"{PREFIX}/admin/queue") + def admin_queue(request: Request, since: int = Query(0, ge=0), limit: int = Query(500, ge=1, le=1000)): + if (r := _admin(request)) is not None: + return r + items = queue.items(since, limit) + return {"items": items, "next": items[-1]["id"] if items else since} + + @api.post(f"{PREFIX}/admin/ack") + def admin_ack(ack: Ack, request: Request): + if (r := _admin(request)) is not None: + return r + return {"deleted": queue.ack(ack.through)} + + return Guard(api) diff --git a/service/deploy/env.example b/service/deploy/env.example new file mode 100644 index 0000000..6acd1c0 --- /dev/null +++ b/service/deploy/env.example @@ -0,0 +1,7 @@ +# /etc/smartbrain-library/env (root:sblibrary, mode 640). Never commit the real file. +LIBRARY_BIND=172.17.0.1 +LIBRARY_DB=/var/lib/smartbrain-library/queue.sqlite3 +LIBRARY_MANIFEST=/etc/smartbrain-library/manifest.json +LIBRARY_TRUSTED_PROXIES=172.16.0.0/12 +# openssl rand -hex 32; the same value goes in LIBRARY_ADMIN_TOKEN on the machine that runs `sourcetool ingest` +LIBRARY_ADMIN_TOKEN= diff --git a/service/deploy/library.caddy b/service/deploy/library.caddy new file mode 100644 index 0000000..3f5da19 --- /dev/null +++ b/service/deploy/library.caddy @@ -0,0 +1,12 @@ + # SmartBrain Library API. Paste INSIDE the existing smartbrain.securecloudgroup.com site block + # ({$LANDING_DOMAIN} { ... } in compose/conf.d/landing.caddy), above file_server. Caddy runs in the + # sb_caddy container, so it reaches the service on the host's docker0 address (see service/README.md). + handle /library/v1/* { + request_body { + max_size 16KB + } + reverse_proxy 172.17.0.1:8095 { + # one entry, the address Caddy saw: the service rate-limits on it and never stores it + header_up X-Forwarded-For {client_ip} + } + } diff --git a/service/deploy/manifest.example.json b/service/deploy/manifest.example.json new file mode 100644 index 0000000..97f0965 --- /dev/null +++ b/service/deploy/manifest.example.json @@ -0,0 +1,6 @@ +{ + "tag": "v1.0.0", + "url": "https://github.com/SecureCloudGroup/SmartBrain_Library/releases/download/v1.0.0/library.duckdb.gz", + "sha256": "", + "bytes": 0 +} diff --git a/service/deploy/smartbrain-library.service b/service/deploy/smartbrain-library.service new file mode 100644 index 0000000..7e894ad --- /dev/null +++ b/service/deploy/smartbrain-library.service @@ -0,0 +1,48 @@ +# SmartBrain Library API — /etc/systemd/system/smartbrain-library.service (see service/README.md) +[Unit] +Description=SmartBrain Library API (votes, suggestions, pack manifest) +After=network-online.target docker.service +Wants=network-online.target + +[Service] +Type=simple +User=sblibrary +Group=sblibrary +WorkingDirectory=/opt/smartbrain-library +EnvironmentFile=/etc/smartbrain-library/env +Environment=PYTHONDONTWRITEBYTECODE=1 +# LIBRARY_BIND comes from the env file: 127.0.0.1, or the docker0 address when Caddy runs in a container +ExecStart=/opt/smartbrain-library/.venv/bin/uvicorn --factory service.app:create_app \ + --host ${LIBRARY_BIND} --port 8095 --workers 1 --limit-concurrency 64 --timeout-keep-alive 5 \ + --no-access-log --no-server-header +Restart=on-failure +RestartSec=3 +StateDirectory=smartbrain-library +StateDirectoryMode=0700 +UMask=0077 + +# hardening: an unprivileged user that can write only its own state directory +NoNewPrivileges=yes +CapabilityBoundingSet= +AmbientCapabilities= +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +ProtectClock=yes +ProtectHostname=yes +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +LockPersonality=yes +SystemCallArchitectures=native +MemoryMax=256M +TasksMax=64 + +[Install] +WantedBy=multi-user.target diff --git a/service/requirements-test.txt b/service/requirements-test.txt new file mode 100644 index 0000000..1441c92 --- /dev/null +++ b/service/requirements-test.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest==9.1.1 diff --git a/service/requirements.txt b/service/requirements.txt new file mode 100644 index 0000000..4d1b48f --- /dev/null +++ b/service/requirements.txt @@ -0,0 +1,6 @@ +# The Library API (service/app.py). httpx is sourcetool's HTTP client (imported with the record rules). +fastapi==0.138.0 +starlette==1.7.0 +pydantic==2.13.4 +uvicorn==0.49.0 +httpx==0.28.1 diff --git a/service/tests/__init__.py b/service/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/tests/test_service.py b/service/tests/test_service.py new file mode 100644 index 0000000..99d08d9 --- /dev/null +++ b/service/tests/test_service.py @@ -0,0 +1,242 @@ +"""The Library API: happy paths, refusals, rate limits, admin auth and the body cap.""" +from __future__ import annotations + +import copy +import json + +import pytest +from fastapi.testclient import TestClient + +from service.app import MAX_BODY, Settings, create_app + +TOKEN = "t" * 40 +V = "/library/v1" + + +def a_record() -> dict: + """What the app's Add-a-source form builds (tier local, the user's own id), values removed.""" + return { + "id": "local-3f2a9c1b7e44", "name": "Harbor water temperature", "description": "Water temperature by station.", + "provider": {"id": "local", "name": "api.harbordata.org", "url": "https://api.harbordata.org", + "authority": "official"}, + "tier": "local", "categories": ["weather/current"], "kinds": ["current_value"], + "coverage": {"geo": "local", "entity": ""}, + "access": {"kind": "http_json", "url_template": "https://api.harbordata.org/v1/temp?station={station}", + "params": [{"name": "station", "kind": "station", "example": None, "required": True}], + "auth": "none", "headers": {}, "docs_url": "https://api.harbordata.org/docs"}, + "terms": {"status": "public_domain", "note": "added by you", "terms_url": ""}, + "freshness": {"cadence": "hourly"}, "examples": [], "notes": "", + "origin": {"by": "user", "at": "2026-09-28T10:00:00Z"}, "validation": {"status": "ok"}, + "votes": {"yes": 999, "no": 0}, + } + + +@pytest.fixture +def make(tmp_path): + def _make(**kw): + manifest = tmp_path / "manifest.json" + s = Settings(db=tmp_path / "q.sqlite3", manifest=manifest, admin_token=TOKEN, **kw) + return TestClient(create_app(s)), manifest + return _make + + +@pytest.fixture +def client(make): + return make()[0] + + +def admin(c, path="/admin/queue", **kw): + return c.get(V + path, headers={"Authorization": f"Bearer {TOKEN}"}, **kw) + + +# ------------------------------------------------------------------------------------------ happy paths +def test_healthz_and_security_headers(client): + r = client.get(V + "/healthz") + assert r.status_code == 200 and r.json() == {"ok": True} + assert r.headers["x-content-type-options"] == "nosniff" + assert r.headers["cache-control"] == "no-store" + assert "default-src 'none'" in r.headers["content-security-policy"] + assert "access-control-allow-origin" not in r.headers + assert "set-cookie" not in r.headers + + +def test_no_cors_and_no_docs(client): + r = client.options(V + "/votes", headers={"Origin": "https://evil.example", + "Access-Control-Request-Method": "POST"}) + assert "access-control-allow-origin" not in r.headers + for p in ("/docs", "/openapi.json", "/redoc"): + assert client.get(p).status_code == 404 + + +def test_vote_is_queued_without_identifiers(client): + r = client.post(V + "/votes", json={"source_id": "noaa-tides-predictions", "verdict": "yes", + "app_version": "0.24.0"}) + assert r.status_code == 202, r.text + items = admin(client).json()["items"] + assert len(items) == 1 + item = items[0] + assert item["kind"] == "vote" + assert item["payload"] == {"source_id": "noaa-tides-predictions", "verdict": "yes", "app_version": "0.24.0"} + assert set(item) == {"id", "kind", "day", "payload"} and len(item["day"]) == 10 # the day, no time + + +def test_suggestion_is_cleaned_server_side(client): + r = client.post(V + "/suggestions", json={"record": a_record(), "via": "form", "app_version": "0.24.0"}) + assert r.status_code == 202, r.text + rec = admin(client).json()["items"][0]["payload"]["record"] + assert rec["tier"] == "harvested" and rec["origin"] == {"by": "user-suggestion"} + assert rec["votes"] == {"yes": 0, "no": 0} and rec["validation"] == {"status": "unvalidated"} + assert rec["terms"]["status"] == "unverified" and rec["provider"]["authority"] == "community" + assert rec["id"] == r.json()["id"] == "api-harbordata-org-harbor-water-temperature" # never the client's id + + +def test_manifest(make): + c, manifest = make() + assert c.get(V + "/manifest").status_code == 503 + m = {"tag": "v1.0.1", "url": "https://github.com/SecureCloudGroup/SmartBrain_Library/releases/download/v1.0.1/" + "library.duckdb.gz", "sha256": "a" * 64, "bytes": 8123456, "extra": "not served"} + manifest.write_text(json.dumps(m)) + r = c.get(V + "/manifest") + assert r.status_code == 200 + assert r.json() == {k: m[k] for k in ("tag", "url", "sha256", "bytes")} + manifest.write_text(json.dumps({**m, "url": "http://insecure.example/pack"})) + assert c.get(V + "/manifest").status_code == 503 + + +# ------------------------------------------------------------------------------------------ refusals +def _suggest(c, record): + return c.post(V + "/suggestions", json={"record": record, "via": "yes", "app_version": "0.24.0"}) + + +@pytest.mark.parametrize("edit, why", [ + (lambda r: r["access"].update(url_template="https://api.harbordata.org/v1/temp?station={station}&api_key=abc123"), + "credential"), + (lambda r: r["access"].update(url_template="https://bob:hunter2@api.harbordata.org/v1/temp?station={station}"), + "credential"), + (lambda r: r["access"].update(url_template="http://api.harbordata.org/v1/temp?station={station}"), "https only"), + (lambda r: r["access"].update(url_template="https://192.168.1.20/v1/temp?station={station}"), "public host"), + (lambda r: r["access"].update(url_template="https://127.0.0.1/v1/temp?station={station}"), "public host"), + (lambda r: r["access"].update(url_template="https://nas.local/v1/temp?station={station}"), "public host"), + (lambda r: r["access"].update(url_template="https://localhost/v1/temp?station={station}"), "public host"), + (lambda r: r["access"].update(docs_url="https://10.0.0.5/docs"), "public host"), + (lambda r: r["access"].update(url_template="https://[::1/v1/temp?station={station}"), "not a valid address"), + (lambda r: r["provider"].update(url="https://[fe80::1]/"), "public host"), + (lambda r: r["access"]["params"][0].update(example="8723214"), "carries a value"), + (lambda r: r["access"].update(headers={"Authorization": "Bearer sk-live-123"}), "credential"), + (lambda r: r.update(categories=["weather/not-a-subcategory"]), "unknown categories"), + (lambda r: r.update(favourite_color="blue"), "unknown fields"), + (lambda r: r["access"].update(secret_sauce=True), "unknown access fields"), + (lambda r: r.update(description="x" * 1500), "longer than"), + (lambda r: r.update(examples=["x"] * 40), "longer than"), + (lambda r: r.update(coverage="US"), "must be an object"), + (lambda r: r.pop("kinds"), "missing kinds"), +]) +def test_suggestion_refusals(client, edit, why): + rec = copy.deepcopy(a_record()) + edit(rec) + r = _suggest(client, rec) + assert r.status_code == 422, r.text + assert any(why in p for p in r.json()["problems"]), r.json() + assert admin(client).json()["items"] == [] + + +@pytest.mark.parametrize("body", [ + {"source_id": "noaa-tides", "verdict": "maybe", "app_version": "0.24.0"}, + {"source_id": "NOAA Tides!", "verdict": "yes", "app_version": "0.24.0"}, + {"source_id": "x" * 121, "verdict": "yes", "app_version": "0.24.0"}, + {"source_id": "noaa-tides", "verdict": "yes", "app_version": "0.24.0", "install_id": "abc"}, + {"source_id": "noaa-tides", "verdict": "yes"}, + {"source_id": "noaa-tides", "verdict": "yes", "app_version": "0.24.0; rm -rf /"}, + {"source_id": 12, "verdict": "yes", "app_version": "0.24.0"}, +]) +def test_vote_refusals(client, body): + r = client.post(V + "/votes", json=body) + assert r.status_code == 422, r.text + assert "rm -rf" not in r.text # problems name the field, never echo the value + + +def test_suggestion_extra_top_level_field(client): + r = client.post(V + "/suggestions", json={"record": a_record(), "via": "form", "app_version": "1", "ask": "tides"}) + assert r.status_code == 422 + + +def test_json_only(client): + r = client.post(V + "/votes", content=b"source_id=x", headers={"Content-Type": "application/x-www-form-urlencoded"}) + assert r.status_code == 415 + r = client.post(V + "/votes", content=b'{"source_id": "noaa-tides", "verdict": "yes", "app_version": "1"}', + headers={"Content-Type": "text/plain"}) + assert r.status_code == 415 + + +def test_body_cap(client): + rec = a_record() + rec["notes"] = "x" * (MAX_BODY + 10) + r = _suggest(client, rec) + assert r.status_code == 413 + # a lying Content-Length is caught while reading, too + body = json.dumps({"record": rec, "via": "yes", "app_version": "1"}).encode() + r = client.post(V + "/suggestions", content=body, headers={"Content-Type": "application/json", + "Content-Length": "100"}) + assert r.status_code == 413 + + +# ------------------------------------------------------------------------------------------ rate limits +def test_vote_rate_limit(make): + c, _ = make(votes_per_hour=3) + body = {"source_id": "noaa-tides", "verdict": "yes", "app_version": "1"} + assert [c.post(V + "/votes", json=body).status_code for _ in range(3)] == [202] * 3 + r = c.post(V + "/votes", json=body) + assert r.status_code == 429 and int(r.headers["retry-after"]) >= 1 + + +def test_suggestion_rate_limit_is_per_client_behind_the_proxy(make): + c, _ = make(suggestions_per_hour=2, trusted_proxies=frozenset({"172.16.0.0/12"})) + c = TestClient(c.app, client=("172.18.0.3", 40000)) # the peer is the Caddy container + body = {"record": a_record(), "via": "form", "app_version": "1"} + one = {"X-Forwarded-For": "203.0.113.7"} + assert [c.post(V + "/suggestions", json=body, headers=one).status_code for _ in range(2)] == [202, 202] + assert c.post(V + "/suggestions", json=body, headers=one).status_code == 429 + # another user behind the same proxy is not affected; a spoofed left-hand entry does not help + assert c.post(V + "/suggestions", json=body, headers={"X-Forwarded-For": "198.51.100.9"}).status_code == 202 + spoof = {"X-Forwarded-For": "1.2.3.4, 203.0.113.7"} + assert c.post(V + "/suggestions", json=body, headers=spoof).status_code == 429 + + +def test_untrusted_peer_cannot_pick_its_address(make): + c, _ = make(votes_per_hour=1) # the test client's peer is not a trusted proxy + body = {"source_id": "noaa-tides", "verdict": "yes", "app_version": "1"} + assert c.post(V + "/votes", json=body, headers={"X-Forwarded-For": "10.0.0.1"}).status_code == 202 + assert c.post(V + "/votes", json=body, headers={"X-Forwarded-For": "10.0.0.2"}).status_code == 429 + + +def test_queue_cap(make): + c, _ = make(max_queue=1) + body = {"source_id": "noaa-tides", "verdict": "yes", "app_version": "1"} + assert c.post(V + "/votes", json=body).status_code == 202 + assert c.post(V + "/votes", json=body).status_code == 503 + + +# ------------------------------------------------------------------------------------------ admin +@pytest.mark.parametrize("headers", [{}, {"Authorization": "Bearer wrong"}, {"Authorization": TOKEN}, + {"Authorization": f"Bearer {TOKEN}x"}, {"Authorization": f"Basic {TOKEN}"}]) +def test_admin_auth(client, headers): + assert client.get(V + "/admin/queue", headers=headers).status_code == 401 + assert client.post(V + "/admin/ack", json={"through": 5}, headers=headers).status_code == 401 + + +@pytest.mark.parametrize("token", ["", "short"]) +def test_admin_disabled_without_a_strong_token(tmp_path, token): + c = TestClient(create_app(Settings(db=tmp_path / "q.sqlite3", admin_token=token))) + assert c.get(V + "/admin/queue", headers={"Authorization": f"Bearer {token}"}).status_code == 401 + + +def test_admin_paging_and_ack(client): + for i in range(5): + client.post(V + "/votes", json={"source_id": f"source-{i}", "verdict": "no", "app_version": "1"}) + page = admin(client, params={"since": 0, "limit": 2}).json() + assert [x["payload"]["source_id"] for x in page["items"]] == ["source-0", "source-1"] + page2 = admin(client, params={"since": page["next"], "limit": 10}).json() + assert len(page2["items"]) == 3 + r = client.post(V + "/admin/ack", json={"through": page["next"]}, headers={"Authorization": f"Bearer {TOKEN}"}) + assert r.json() == {"deleted": 2} + assert len(admin(client).json()["items"]) == 3 diff --git a/sourcetool/__main__.py b/sourcetool/__main__.py index 6c2e3cf..39da3d3 100644 --- a/sourcetool/__main__.py +++ b/sourcetool/__main__.py @@ -11,6 +11,7 @@ python -m sourcetool fills declare how every source parameter is filled python -m sourcetool evalres [resolution_asks|resolution_holdout|resolution_sealed] resolver accuracy python -m sourcetool evallookup [lookup_asks|...] is the first source offered the right one + python -m sourcetool ingest [--dry-run] [--no-probe] [--commit] pull the Library API's votes + suggestions Writes only to this checkout. Publishing is a PR the operator merges. """ @@ -24,7 +25,7 @@ def _files(only: str | None) -> list[Path]: - dirs = [SOURCES / only] if only else [SOURCES / "curated", SOURCES / "harvested"] + dirs = [SOURCES / only] if only else [SOURCES / "curated", SOURCES / "harvested", SOURCES / "suggested"] return sorted(p for d in dirs for p in d.glob("*.jsonl")) @@ -160,9 +161,14 @@ def cmd_evallookup(args) -> int: return rc +def cmd_ingest(args) -> int: + from . import ingest + return ingest.main(args) + + def main(argv: list[str]) -> int: if not argv or argv[0] not in {"check", "validate", "harvest", "build", "lookup", "coverage", "resolvers", - "policies", "fills", "evalres", "evallookup"}: + "policies", "fills", "evalres", "evallookup", "ingest"}: print(__doc__) return 2 return globals()["cmd_" + argv[0]](argv[1:]) diff --git a/sourcetool/build.py b/sourcetool/build.py index 64798a9..5c6f145 100644 --- a/sourcetool/build.py +++ b/sourcetool/build.py @@ -61,7 +61,8 @@ def build() -> str: BUILD.mkdir(exist_ok=True) if DB.exists(): DB.unlink() - recs = [r for d in ("curated", "harvested") for f in sorted((SOURCES / d).glob("*.jsonl")) for r in read_jsonl(f)] + recs = [r for d in ("curated", "harvested", "suggested") for f in sorted((SOURCES / d).glob("*.jsonl")) + for r in read_jsonl(f)] con = duckdb.connect(str(DB)) con.execute("""CREATE TABLE library_sources( id VARCHAR PRIMARY KEY, name VARCHAR, description VARCHAR, provider_id VARCHAR, provider_name VARCHAR, @@ -307,7 +308,8 @@ def named_entities(ask: str) -> list[str]: from . import resolve from .resolvers import norm words = set(norm(ask).split()) - codes = set(_re.findall(r"\b[A-Z]{3,5}\b", ask or "")) + # a code that names the ask's place ("NYC weather") is the place, not a ticker, unless a cue says so + codes = set(_re.findall(r"\b[A-Z]{3,5}\b", ask or "")) - {w.upper() for w in place_words(ask)[0]} found = {} for res in ENTITY_RESOLVERS: r = resolve.by_name(res, ask) diff --git a/sourcetool/common.py b/sourcetool/common.py index 322be49..53ab0b1 100644 --- a/sourcetool/common.py +++ b/sourcetool/common.py @@ -93,10 +93,21 @@ def _raw_get(url: str, *, check_robots: bool = True, headers: dict | None = None _last_hit[host] = time.monotonic() +def _cache_path(url: str, headers: dict | None) -> Path: + key = hashlib.sha256((url + json.dumps(headers or {}, sort_keys=True)).encode()).hexdigest() + return CACHE / key[:2] / key + + +def uncache(url: str, headers: dict | None = None) -> None: + """Forget a cached answer that turned out unusable (a response cut off mid-body).""" + cp = _cache_path(url, headers) + cp.unlink(missing_ok=True) + cp.with_suffix(".meta").unlink(missing_ok=True) + + def get(url: str, *, cache_hours: float = 24.0, headers: dict | None = None, api: bool = False) -> tuple[int, bytes, str]: """Polite cached GET. Returns (status, body, content_type).""" - key = hashlib.sha256((url + json.dumps(headers or {}, sort_keys=True)).encode()).hexdigest() - cp = CACHE / key[:2] / key + cp = _cache_path(url, headers) if cp.exists() and (time.time() - cp.stat().st_mtime) < cache_hours * 3600: meta = json.loads((cp.with_suffix(".meta")).read_text()) return meta["status"], cp.read_bytes(), meta["ctype"] diff --git a/sourcetool/ingest.py b/sourcetool/ingest.py new file mode 100644 index 0000000..d79a6d8 --- /dev/null +++ b/sourcetool/ingest.py @@ -0,0 +1,162 @@ +"""`sourcetool ingest`: pull the Library API's queue (docs/PLAN.md section 4) into this checkout. + + * every item is checked again with the same rules the service ran (`sourcetool/submission.py`); + * votes are added to each source's `votes` (a "broken" report counts as a No and is listed, so the + operator can re-validate that source); + * new suggestions go to `sources/suggested/.jsonl` with tier `harvested` and + origin `{by: "user-suggestion"}`, and are probed with `validate`; a suggestion of a source the Library + already has (same address) counts as a Yes for it when it came from a Yes; + * the queue is then acknowledged (cleared) up to the last item read. + +The operator reviews the diff and opens the PR (`--commit` makes the branch and commit; it never pushes). + +Environment: LIBRARY_API_URL (default https://smartbrain.securecloudgroup.com/library/v1) and +LIBRARY_ADMIN_TOKEN (the service's admin token). +""" +from __future__ import annotations + +import collections +import os +import subprocess +import time +from pathlib import Path +from urllib.parse import urlsplit + +import httpx + +from .common import ROOT, SOURCES, USER_AGENT, read_jsonl, write_jsonl +from .submission import VIA, clean_suggestion, vote_problems + +DEFAULT_API = "https://smartbrain.securecloudgroup.com/library/v1" +SOURCE_DIRS = ("curated", "harvested", "suggested") + + +def _address(r: dict) -> str: + a = r.get("access", {}) + return (a.get("url_template") or a.get("docs_url") or "").strip().lower() + + +def fetch(api: str, token: str) -> list[dict]: + """Every queued item, oldest first.""" + items: list[dict] = [] + since = 0 + with httpx.Client(headers={"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"}, timeout=60.0, + trust_env=False) as c: + while True: + r = c.get(f"{api}/admin/queue", params={"since": since, "limit": 1000}) + r.raise_for_status() + page = r.json() + if not page["items"]: + return items + items += page["items"] + since = page["next"] + + +def acknowledge(api: str, token: str, through: int) -> int: + with httpx.Client(headers={"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"}, timeout=60.0, + trust_env=False) as c: + r = c.post(f"{api}/admin/ack", json={"through": through}) + r.raise_for_status() + return r.json()["deleted"] + + +def apply(items: list[dict], *, sources: Path = SOURCES, probe: bool = True, write: bool = True, + today: str | None = None) -> dict: + """Fold the queue into the source files. Returns a summary (what `ingest` prints).""" + today = today or time.strftime("%Y-%m-%d", time.gmtime()) + files = {f: read_jsonl(f) for d in SOURCE_DIRS for f in sorted((sources / d).glob("*.jsonl"))} + by_id = {r["id"]: (f, r) for f, rows in files.items() for r in rows} + by_address = {_address(r): r["id"] for _, r in by_id.values() if _address(r)} + tally: dict[str, collections.Counter] = collections.defaultdict(collections.Counter) + new: dict[str, dict] = {} # address -> the new record + s = collections.Counter() + unknown: collections.Counter = collections.Counter() + for item in items: + p = item.get("payload") or {} + if item.get("kind") == "vote": + if vote_problems(p.get("source_id"), p.get("verdict")): + s["rejected"] += 1 + elif p["source_id"] not in by_id: + unknown[p["source_id"]] += 1 + else: + tally[p["source_id"]][p["verdict"]] += 1 + continue + rec, problems = clean_suggestion(p.get("record")) if item.get("kind") == "suggestion" else ({}, ["kind"]) + if problems or p.get("via") not in VIA: + s["rejected"] += 1 + continue + known = by_address.get(_address(rec)) + if known: # the Library already has it: a Yes on it is a Yes for that source + if p["via"] == "yes": + tally[known]["yes"] += 1 + else: + s["duplicate suggestions"] += 1 + continue + first = new.setdefault(_address(rec), rec) + if first is rec: + rec["origin"] = {"by": "user-suggestion", "at": today, "via": p["via"]} + s["new suggestions"] += 1 + else: + s["duplicate suggestions"] += 1 + if p["via"] == "yes": + first["votes"]["yes"] += 1 + + changed: set[Path] = set() + for sid, c in tally.items(): + f, r = by_id[sid] + v = r.setdefault("votes", {"yes": 0, "no": 0}) + v["yes"] = v.get("yes", 0) + c["yes"] + v["no"] = v.get("no", 0) + c["no"] + c["broken"] + changed.add(f) + s["votes"] += sum(c.values()) + taken = set(by_id) + for rec in new.values(): + base, n = rec["id"], 2 + while rec["id"] in taken: # a different source already holds this id + rec["id"] = f"{base[:115]}-{n}" + n += 1 + taken.add(rec["id"]) + if new and probe: + from .validate import validate_all + validate_all(list(new.values())) + if write: + for f in changed: + write_jsonl(f, files[f]) + if new: + out = sources / "suggested" / f"{today}.jsonl" + write_jsonl(out, read_jsonl(out) + list(new.values())) + return {"counts": dict(s), "new": sorted(new[a]["id"] for a in new), + "broken": sorted(sid for sid, c in tally.items() if c["broken"]), "unknown_ids": dict(unknown), + "files": sorted(str(f.relative_to(sources)) for f in changed)} + + +def _api() -> tuple[str, str]: + api = os.environ.get("LIBRARY_API_URL", DEFAULT_API).rstrip("/") + token = os.environ.get("LIBRARY_ADMIN_TOKEN", "") + u = urlsplit(api) + if u.scheme != "https" and u.hostname not in ("127.0.0.1", "localhost"): + raise SystemExit("LIBRARY_API_URL must be https (the admin token must never travel in the clear)") + if not token: + raise SystemExit("set LIBRARY_ADMIN_TOKEN (the service's admin token)") + return api, token + + +def main(args: list[str]) -> int: + api, token = _api() + dry = "--dry-run" in args + items = fetch(api, token) + summary = apply(items, probe="--no-probe" not in args and not dry, write=not dry) + print(f"{len(items)} queued items: {summary['counts']}") + for k in ("new", "broken", "unknown_ids", "files"): + if summary[k]: + print(f" {k}: {summary[k]}") + if dry or not items: + return 0 + print(f"acknowledged {acknowledge(api, token, max(i['id'] for i in items))} items on the service") + if "--commit" in args: + branch = f"ingest-{time.strftime('%Y-%m-%d', time.gmtime())}" + subprocess.run(["git", "checkout", "-b", branch], cwd=ROOT, check=True) + subprocess.run(["git", "add", "sources"], cwd=ROOT, check=True) + subprocess.run(["git", "commit", "-m", f"Library ingest: {summary['counts']}"], cwd=ROOT, check=True) + print(f"committed on {branch}; review it and open the PR") + return 0 diff --git a/sourcetool/resolve.py b/sourcetool/resolve.py index 64883b1..93a6082 100644 --- a/sourcetool/resolve.py +++ b/sourcetool/resolve.py @@ -87,6 +87,11 @@ def by_name(resolver: str, ask: str, *, raw_case_codes: bool = True, many: bool upper = set(re.findall(r"\b[A-Z0-9$]{1,6}\b", ask or "")) # tokens the user typed in capitals states = states_in(ask) common = _common_words() + # a capitalised state code that is also a place's nickname ("LA", "DC") names that place only when no + # other place is named: "LA weather" is Los Angeles, "Lafayette LA" is Lafayette, Louisiana + codes = {c.lower() for c in re.findall(r"\b([A-Z]{2})\b", ask or "") if c in US_STATES} \ + if resolver == "place" else set() + via_other: set[int] = set() # candidates named by something other than a bare state code scored: dict[int, float] = {} first_at: dict[int, int] = {} text = f" {' '.join(tokens)} " @@ -110,16 +115,23 @@ def by_name(resolver: str, ask: str, *, raw_case_codes: bool = True, many: bool if n == 1 and r["kind"] == "crypto_asset" and gram in r["aliases"] and gram != norm(r["name"]) \ and not r.get("rank") and gram.upper() not in upper: continue # an unranked token's lowercase symbol is noise ("eth" is Ethereum, not a clone) + if n == 1 and r["kind"] == "place" and len(gram) <= 2 and gram.upper() not in upper: + continue # a two-letter place nickname counts only in capitals ("LA", never the word "la") score = (2.0 * spec + partial_penalty + (0.5 if gram == norm(r["name"]) else 0.0) + RANK_WEIGHT.get(resolver, 0.05) * float(r.get("rank") or 0)) - if states: - score += 3.0 if r.get("state") in states else (-3.0 if r.get("state") else 0.0) + said = states - {gram.upper()} if n == 1 and gram in codes else states + if said: + score += 3.0 if r.get("state") in said else (-3.0 if r.get("state") else 0.0) + if not (n == 1 and gram in codes): + via_other.add(idx) for k in ATTR_CONTEXT: # "Alabama football", "NBA", "on the NYSE" v = norm(str(r.get("attrs", {}).get(k) or "")) if v and any(f" {w} " in text for w in {v, v.replace("college ", "")} if w): score += 1.5 scored[idx] = max(scored.get(idx, -1e9), score) first_at[idx] = min(first_at.get(idx, 99), _i) + if via_other and codes: # another place is named, so the code is its state ("Lafayette LA") + scored = {i: sc for i, sc in scored.items() if i in via_other} if not scored: return {"status": "none", "best": None, "candidates": [], "reason": f"nothing in {resolver} matches"} ranked = sorted(scored.items(), key=lambda kv: -kv[1]) diff --git a/sourcetool/resolvers.py b/sourcetool/resolvers.py index aa612e1..2fee57b 100644 --- a/sourcetool/resolvers.py +++ b/sourcetool/resolvers.py @@ -21,9 +21,10 @@ import math import re import zipfile +from urllib.parse import quote from xml.etree import ElementTree -from .common import ROOT, get, get_json, write_jsonl +from .common import ROOT, get, get_json, uncache, write_jsonl RES = ROOT / "resolvers" @@ -94,8 +95,11 @@ def _place_population() -> dict[str, int]: def h_place() -> list[dict]: out = [] pop = _place_population() + gnis: dict[str, str] = {} # GNIS feature id (the gazetteer's ANSICODE) -> GEOID, for Wikidata's nicknames for r in _gazetteer("https://www2.census.gov/geo/docs/maps-data/data/gazetteer/2024_Gazetteer/" "2024_Gaz_place_national.zip"): + if r.get("ANSICODE", "").isdigit(): + gnis[str(int(r["ANSICODE"]))] = r["GEOID"] st, full = r["USPS"], r["NAME"] name = _PLACE_SUFFIX.sub("", re.sub(r"\s*\(balance\)$", "", full)).strip() typ = full[len(name):].strip().lower() or "place" @@ -111,9 +115,78 @@ def h_place() -> list[dict]: float(r["INTPTLAT"]), float(r["INTPTLONG"]), st, {"type": typ, "land_km2": round(float(r["ALAND"]) / 1e6, 1), "pop": pop.get(r["GEOID"], 0)}, rank=round(math.log10(max(pop.get(r["GEOID"], 0), 1)), 2))) + _add_nicknames(out, gnis) return out +# ------------------------------------------------------------------------------------------ place nicknames +_NICKNAME_QUERY = """SELECT ?gnis ?nick WHERE {{ ?item wdt:{prop} ?nick . FILTER(LANGMATCHES(LANG(?nick), "en")) + ?item wdt:P590 ?gnis . }}""" + + +def _wikidata_nicknames() -> list[tuple[str, str]]: + """(GNIS id, nickname) from Wikidata: English short names (P1813: "NYC", "LA") and nicknames (P1449: + "Big Apple", "NOLA") of items that carry a GNIS id (P590), the id the Census gazetteer carries as + ANSICODE. Joining on it keeps exactly the items that ARE a Census place (not a team, county or + neighbourhood of the same name), which is stricter than a city/town class filter.""" + out = [] + for prop in ("P1813", "P1449"): + url = "https://query.wikidata.org/sparql?format=json&query=" + quote(_NICKNAME_QUERY.format(prop=prop)) + headers = {"Accept": "application/sparql-results+json"} + _, body, _ = get(url, cache_hours=720, api=True, headers=headers) + try: + rows = json.loads(body)["results"]["bindings"] + except ValueError: # the query service cut the answer off at its time limit: never keep that + uncache(url, headers) + raise + out += [(b["gnis"]["value"], b["nick"]["value"]) for b in rows] + return out + + +def _nickname_core(nick: str) -> str: + """The form of a Wikidata nickname people type, or "" when it would misfire as a place word. + + Kept: codes written in capitals ("NYC", "L.A.", "NOLA"), single words of 5+ letters ("Philly", "Vegas"), + and phrases of up to three words ("Big Apple", "Mile High City"), with a leading "the" dropped (the + matcher ignores "the" anyway). Dropped: slogans (4+ words), anything with a digit ("The 313"), short + everyday words ("Jeff", "The Hub", "The Land"), and state names.""" + raw = re.sub(r"^\s*the\s+", "", nick.strip().strip('"'), flags=re.I) + core = norm(raw) + words = core.split() + if not words or len(words) > 3 or re.search(r"\d", core) or core in _STATE_BY_NAME: + return "" + code = re.sub(r"[^A-Za-z]", "", raw) + if len(words) == 1 and not (code.isupper() and 2 <= len(code) <= 5) and len(core) < 5: + return "" + from .resolve import _common_words + return "" if len(words) == 1 and core in _common_words() else core + + +def _add_nicknames(places: list[dict], gnis: dict[str, str]) -> None: + """Nicknames and short names become extra aliases on their place (docs/RESOLVERS.md, "Place nicknames"). + + Two sources: Wikidata (filtered by ``_nickname_core``, minus the reviewed file's ``exclude`` list) and the + reviewed ``entries`` in ``resolvers/place_nicknames.json`` for the common ones Wikidata lacks (it has no + nickname for Chicago). + A Wikidata nickname never shadows a real place's own name: "Frisco" is Frisco, Texas, not San Francisco, + and "Queen City" is a town in Texas and Missouri, so neither is added.""" + reviewed = json.loads((RES / "place_nicknames.json").read_text()) + refused = {norm(n) for n in reviewed["exclude"]} + by_key = {p["key"]: p for p in places} + taken: set[str] = {a for p in places for a in p["aliases"]} + for gid, nick in _wikidata_nicknames(): + p = by_key.get(gnis.get(gid, "")) + core = _nickname_core(nick) + if p and core and core not in taken and norm(nick) not in refused: + p["aliases"] = sorted({*p["aliases"], core}) + for e in reviewed["entries"]: + named = [p for p in places if p["state"] == e["state"] and norm(e["place"]) in p["aliases"]] + if not named: + raise ValueError(f"place_nicknames.json: no place {e['place']!r} in {e['state']}") + p = max(named, key=lambda p: p["attrs"].get("pop") or 0) + p["aliases"] = sorted({*p["aliases"], *(norm(n) for n in e["nicknames"])}) + + def h_zip() -> list[dict]: return [_e("zip", "zip", r["GEOID"], r["GEOID"], [], float(r["INTPTLAT"]), float(r["INTPTLONG"])) for r in _gazetteer("https://www2.census.gov/geo/docs/maps-data/data/gazetteer/2024_Gazetteer/" diff --git a/sourcetool/schema.py b/sourcetool/schema.py index ecfe943..3ebd291 100644 --- a/sourcetool/schema.py +++ b/sourcetool/schema.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +import ipaddress import re from urllib.parse import urlsplit @@ -36,6 +37,33 @@ def _cat_ids() -> tuple[set[str], set[str], set[str]]: _CATS, _KINDS, _PKINDS = _cat_ids() +def url_problems(url: str) -> list[str]: + """https, a public host, and no credential in the address.""" + errs = [] + try: + u = urlsplit(url.replace("{", "").replace("}", "")) + host_is_param = bool(re.fullmatch(r"\{[a-z_][a-z0-9_]*\}", urlsplit(url).netloc)) + except ValueError: + return ["not a valid address"] + if u.scheme != "https": + errs.append("https only") + # a {param} host is filled by a resolver whose values are checked when filled + if not host_is_param and (not u.hostname or "." not in u.hostname or _private_ip(u.hostname) + or u.hostname.endswith((".local", ".internal", ".localhost", ".home.arpa"))): + errs.append("public host only") + if (_SECRETISH.search(url) and "{" not in _SECRETISH.search(url).group(2)) or "@" in urlsplit(url).netloc: + errs.append("url carries a credential; keys come from the user's own vault as a {param}") + return errs + + +def _private_ip(host: str) -> bool: + """An address typed as an IP that is not on the public internet (10.x, 192.168.x, 127.x, 169.254.x...).""" + try: + return not ipaddress.ip_address(host).is_global + except ValueError: + return False # a host name, not an IP literal + + def validate_record(r: dict) -> list[str]: """Return a list of problems; an empty list means the record is well-formed.""" errs: list[str] = [] @@ -67,16 +95,7 @@ def validate_record(r: dict) -> list[str]: if not url: errs.append("access needs url_template or docs_url") else: - u = urlsplit(url.replace("{", "").replace("}", "")) - host_is_param = bool(re.fullmatch(r"\{[a-z_][a-z0-9_]*\}", urlsplit(url).netloc)) - if u.scheme != "https": - errs.append("https only") - # a {param} host is filled by a resolver whose values are checked when filled - if not host_is_param and (not u.hostname or "." not in u.hostname - or u.hostname.endswith((".local", ".internal"))): - errs.append("public host only") - if _SECRETISH.search(url) and "{" not in _SECRETISH.search(url).group(2): - errs.append("url carries a credential; keys come from the user's own vault as a {param}") + errs += url_problems(url) names = set(_PARAM.findall(a.get("url_template") or "")) declared = {q["name"] for q in a.get("params", [])} if names - declared: diff --git a/sourcetool/submission.py b/sourcetool/submission.py new file mode 100644 index 0000000..f8888f1 --- /dev/null +++ b/sourcetool/submission.py @@ -0,0 +1,151 @@ +"""What the Library API accepts from the app (docs/PLAN.md sections 3-4), checked the same way by the +service on the VPS (`service/app.py`) and again by `sourcetool ingest` on the operator's side. + +A vote names a Library source by id. A suggestion is a new source record with its parameter VALUES +removed: the template and the parameter kinds are kept, the user's station, ticker or place are not. +Nothing the client says about trust is kept: the tier, votes, validation, terms status, authority and +origin are set here, and the id is derived from the record, never taken from the client. +""" +from __future__ import annotations + +import json +import re +from urllib.parse import urlsplit + +from .common import slug +from .schema import _ID, url_problems, validate_record + +VERDICTS = ("yes", "no", "broken") +VIA = ("form", "yes") +APP_VERSION = re.compile(r"^[0-9A-Za-z][0-9A-Za-z.+-]{0,31}$") + +# fields a suggestion may carry, and the ones the server owns (silently dropped, then set here) +ACCEPTED = {"name", "description", "provider", "categories", "kinds", "coverage", "access", "terms", "freshness", + "examples", "notes"} +SERVER_OWNED = {"id", "tier", "votes", "validation", "signals", "origin", "role", "audience"} +ACCESS_KEYS = {"kind", "url_template", "docs_url", "params", "auth", "headers", "contact_ua"} +PARAM_KEYS = {"name", "kind", "required", "example"} +_PARAM_NAME = re.compile(r"^[a-z_][a-z0-9_]{0,39}$") +_SECRET_HEADER = re.compile(r"auth|token|key|secret|password|session|cookie", re.I) + +MAX_RECORD_BYTES = 12_000 +MAX_STR = 2048 +MAX_LIST = 32 +LIMITS = {"name": 200, "description": 1000, "notes": 1000} + + +def vote_problems(source_id: object, verdict: object) -> list[str]: + errs = [] + if not isinstance(source_id, str) or not _ID.match(source_id): + errs.append("source_id must be a Library id (a lowercase slug of at most 120 characters)") + if verdict not in VERDICTS: + errs.append(f"verdict must be one of {', '.join(VERDICTS)}") + return errs + + +def _walk(v: object, depth: int = 0) -> list[str]: + if depth > 5: + return ["record nests too deeply"] + if isinstance(v, str): + return [f"a text field is longer than {MAX_STR} characters"] if len(v) > MAX_STR else [] + if isinstance(v, list): + return ([f"a list is longer than {MAX_LIST} items"] if len(v) > MAX_LIST else []) + \ + [e for x in v for e in _walk(x, depth + 1)] + if isinstance(v, dict): + return [e for k, x in v.items() for e in _walk(k, depth + 1) + _walk(x, depth + 1)] + if v is None or isinstance(v, (bool, int, float)): + return [] + return ["unsupported value"] + + +def _shape_problems(r: dict) -> list[str]: + """Types first, so the schema rules never meet a value of the wrong type.""" + errs = [] + for k in ("name", "description"): + if not isinstance(r.get(k), str) or not r[k].strip(): + errs.append(f"{k} is required text") + for k, n in LIMITS.items(): + if isinstance(r.get(k), str) and len(r[k]) > n: + errs.append(f"{k} is longer than {n} characters") + for k in ("provider", "coverage", "access", "terms", "freshness"): + if not isinstance(r.get(k), dict): + errs.append(f"{k} must be an object") + if isinstance(r.get("provider"), dict) and not isinstance(r["provider"].get("name"), str): + errs.append("provider.name is required text") + for k in ("categories", "kinds", "examples"): + if k in r and (not isinstance(r[k], list) or not all(isinstance(x, str) for x in r[k])): + errs.append(f"{k} must be a list of text") + if "notes" in r and not isinstance(r["notes"], str): + errs.append("notes must be text") + a = r.get("access") + if isinstance(a, dict): + if set(a) - ACCESS_KEYS: + errs.append(f"unknown access fields {sorted(set(a) - ACCESS_KEYS)}") + for k in ("url_template", "docs_url"): + if k in a and not isinstance(a[k], str): + errs.append(f"access.{k} must be text") + if not isinstance(a.get("params", []), list) or not all(isinstance(q, dict) for q in a.get("params", [])): + errs.append("access.params must be a list of objects") + h = a.get("headers", {}) + if not isinstance(h, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in h.items()): + errs.append("access.headers must map names to text") + return errs + + +def _access_problems(a: dict) -> list[str]: + errs = [] + for q in a.get("params", []): + if set(q) - PARAM_KEYS: + errs.append(f"unknown param fields {sorted(set(q) - PARAM_KEYS)}") + if not isinstance(q.get("name"), str) or not _PARAM_NAME.match(q["name"]): + errs.append("param name must be a short lowercase identifier") + if q.get("example") not in (None, ""): + errs.append(f"param {q.get('name')!r} carries a value; parameter values are removed before sending") + for name, value in a.get("headers", {}).items(): + if _SECRET_HEADER.search(name) and "{key}" not in value: + errs.append(f"header {name!r} carries a credential; keys come from the user's own vault as {{key}}") + if a.get("docs_url") and a.get("url_template"): + errs += [f"docs_url: {e}" for e in url_problems(a["docs_url"])] + return errs + + +def suggestion_id(r: dict) -> str: + """A stable id from where the source lives and what it is called (never the client's own id).""" + a = r["access"] + url = (a.get("url_template") or a.get("docs_url") or "").replace("{", "").replace("}", "") + try: + host = urlsplit(url).hostname + except ValueError: # a malformed address: validate_record names the problem + host = "" + sid = slug(host or "", r["name"])[:120].strip("-") + return sid if _ID.match(sid) else slug("suggested", sid)[:120].strip("-") + + +def clean_suggestion(record: object) -> tuple[dict, list[str]]: + """(the record as the Library would hold it, problems). An empty problem list means accepted.""" + if not isinstance(record, dict): + return {}, ["record must be an object"] + errs = _walk(record) + if len(json.dumps(record)) > MAX_RECORD_BYTES: + errs.append(f"record is larger than {MAX_RECORD_BYTES} bytes") + unknown = set(record) - ACCEPTED - SERVER_OWNED + if unknown: + errs.append(f"unknown fields {sorted(unknown)}") + rec = {k: v for k, v in record.items() if k in ACCEPTED} + errs += _shape_problems(rec) + if errs: + return {}, errs + errs += _access_problems(rec["access"]) + if rec["provider"].get("url"): + errs += [f"provider.url: {e}" for e in url_problems(str(rec["provider"]["url"]))] + # trust is decided in review, never by the client + rec["provider"] = {**rec["provider"], "authority": "community"} + rec["terms"] = {**rec["terms"], "status": "unverified"} + rec["access"] = {**rec["access"], "params": [{**q, "example": None} for q in rec["access"].get("params", [])]} + rec.setdefault("examples", []) + rec.setdefault("notes", "") + rec.update(tier="harvested", origin={"by": "user-suggestion"}, validation={"status": "unvalidated"}, + votes={"yes": 0, "no": 0}) + rec["id"] = suggestion_id(rec) + errs += validate_record(rec) + return (rec, []) if not errs else ({}, errs) diff --git a/tests/lookup_asks.json b/tests/lookup_asks.json index d3890b1..83bdbc5 100644 --- a/tests/lookup_asks.json +++ b/tests/lookup_asks.json @@ -1,6 +1,8 @@ {"description": "Source lookup (development): ask -> acceptable FIRST source ids. Across every category.", "cases": [ {"ask": "weather in Boise", "ok": ["open-meteo-forecast", "nws-forecast", "nws-forecast-hourly", "wttr-in"]}, + {"ask": "NYC weather", "ok": ["open-meteo-forecast", "nws-forecast", "nws-forecast-hourly", "wttr-in"]}, + {"ask": "air quality in LA", "ok": ["open-meteo-air-quality", "airnow-current-zip", "airnow-forecast-zip"]}, {"ask": "will it rain tomorrow in Denver", "ok": ["open-meteo-forecast", "nws-forecast", "nws-forecast-hourly"]}, {"ask": "current temperature in Chicago", "ok": ["open-meteo-forecast", "nws-observation-latest", "wttr-in", "awc-metar"]}, {"ask": "severe weather alerts near me", "ok": ["nws-alerts-point", "nws-alerts-state"]}, diff --git a/tests/resolution_asks.json b/tests/resolution_asks.json index 4bfb75f..b7e760d 100644 --- a/tests/resolution_asks.json +++ b/tests/resolution_asks.json @@ -323,6 +323,72 @@ "resolver": "county", "expect": "resolved", "key": "17031" + }, + { + "ask": "NYC weather", + "resolver": "place", + "expect": "resolved", + "state": "NY" + }, + { + "ask": "LA weather", + "resolver": "place", + "expect": "resolved", + "state": "CA" + }, + { + "ask": "SF fog tonight", + "resolver": "place", + "expect": "resolved", + "state": "CA" + }, + { + "ask": "Philly forecast", + "resolver": "place", + "expect": "resolved", + "state": "PA" + }, + { + "ask": "Vegas weather this weekend", + "resolver": "place", + "expect": "resolved", + "state": "NV" + }, + { + "ask": "DC weather", + "resolver": "place", + "expect": "resolved", + "state": "DC" + }, + { + "ask": "NOLA weather", + "resolver": "place", + "expect": "resolved", + "state": "LA" + }, + { + "ask": "Chi-town weather", + "resolver": "place", + "expect": "resolved", + "state": "IL" + }, + { + "ask": "weather in the Big Apple", + "resolver": "place", + "expect": "resolved", + "state": "NY" + }, + { + "ask": "weather in Lafayette LA", + "resolver": "place", + "expect": "resolved", + "state": "LA" + }, + { + "ask": "weather in la crosse", + "resolver": "place", + "expect": "resolved", + "state": "WI" } ], "near": [ diff --git a/tests/test_ingest.py b/tests/test_ingest.py new file mode 100644 index 0000000..7afae07 --- /dev/null +++ b/tests/test_ingest.py @@ -0,0 +1,117 @@ +"""`sourcetool ingest` against a fake queue: vote aggregation, suggestions, duplicates, refusals.""" +from __future__ import annotations + +import copy +import json + +import pytest + +from sourcetool.common import read_jsonl, write_jsonl +from sourcetool.ingest import apply + + +def _record(sid: str, url: str) -> dict: + return {"id": sid, "name": sid, "description": "d", "provider": {"name": "P", "authority": "official"}, + "tier": "curated", "categories": ["weather/current"], "kinds": ["current_value"], + "coverage": {"geo": "US"}, "access": {"kind": "http_json", "url_template": url, "params": [], + "auth": "none", "headers": {}}, + "terms": {"status": "public_domain"}, "freshness": {"cadence": "hourly"}, "origin": {"by": "test"}, + "votes": {"yes": 2, "no": 1}} + + +def _suggested(name: str, url: str) -> dict: + return {"name": name, "description": "Water temperature.", "provider": {"name": "Harbor", "authority": "community"}, + "categories": ["weather/current"], "kinds": ["current_value"], "coverage": {"geo": "US-CA"}, + "access": {"kind": "http_json", "url_template": url, "auth": "none", "headers": {}, + "params": [{"name": "station", "kind": "station", "example": None, "required": True}]}, + "terms": {"status": "unverified"}, "freshness": {"cadence": "hourly"}} + + +def vote(i, sid, verdict): + return {"id": i, "kind": "vote", "day": "2026-09-28", "payload": {"source_id": sid, "verdict": verdict, + "app_version": "0.24.0"}} + + +def suggestion(i, record, via): + return {"id": i, "kind": "suggestion", "day": "2026-09-28", + "payload": {"record": record, "via": via, "app_version": "0.24.0"}} + + +@pytest.fixture +def sources(tmp_path): + write_jsonl(tmp_path / "curated" / "core.jsonl", [_record("noaa-tides", "https://api.tides.gov/{station}"), + _record("nws-alerts", "https://api.weather.gov/alerts")]) + return tmp_path + + +def test_votes_aggregate_per_source(sources): + items = [vote(1, "noaa-tides", "yes"), vote(2, "noaa-tides", "yes"), vote(3, "noaa-tides", "no"), + vote(4, "nws-alerts", "broken"), vote(5, "gone-source", "yes"), vote(6, "Bad Id!", "yes"), + vote(7, "nws-alerts", "maybe")] + s = apply(items, sources=sources, probe=False, today="2026-09-28") + rows = {r["id"]: r for r in read_jsonl(sources / "curated" / "core.jsonl")} + assert rows["noaa-tides"]["votes"] == {"yes": 4, "no": 2} + assert rows["nws-alerts"]["votes"] == {"yes": 2, "no": 2} # broken counts against the source + assert s["broken"] == ["nws-alerts"] and s["unknown_ids"] == {"gone-source": 1} + assert s["counts"] == {"rejected": 2, "votes": 4} + + +def test_new_suggestions_are_written_merged_and_voted(sources): + rec = _suggested("Harbor temperature", "https://api.harbordata.org/v1/temp?station={station}") + items = [suggestion(1, rec, "form"), suggestion(2, copy.deepcopy(rec), "yes"), + suggestion(3, copy.deepcopy(rec), "yes")] + s = apply(items, sources=sources, probe=False, today="2026-09-28") + out = read_jsonl(sources / "suggested" / "2026-09-28.jsonl") + assert len(out) == 1 + r = out[0] + assert r["id"] == "api-harbordata-org-harbor-temperature" and s["new"] == [r["id"]] + assert r["tier"] == "harvested" + assert r["origin"] == {"by": "user-suggestion", "at": "2026-09-28", "via": "form"} + assert r["votes"] == {"yes": 2, "no": 0} + assert s["counts"] == {"new suggestions": 1, "duplicate suggestions": 2} + + +def test_a_suggestion_of_a_known_source_is_a_yes_for_it(sources): + known = _suggested("Tides", "https://api.tides.gov/{station}") + s = apply([suggestion(1, known, "yes"), suggestion(2, copy.deepcopy(known), "form")], sources=sources, + probe=False, today="2026-09-28") + rows = {r["id"]: r for r in read_jsonl(sources / "curated" / "core.jsonl")} + assert rows["noaa-tides"]["votes"] == {"yes": 3, "no": 1} + assert not (sources / "suggested").exists() + assert s["counts"] == {"duplicate suggestions": 1, "votes": 1} + + +def test_ingest_checks_every_item_again(sources): + bad = _suggested("Leaky", "https://api.harbordata.org/v1/temp?station={station}&api_key=abc123") + valued = _suggested("Valued", "https://api.harbordata.org/v2/temp?station={station}") + valued["access"]["params"][0]["example"] = "8723214" + private = _suggested("Mine", "https://192.168.1.4/temp?station={station}") + s = apply([suggestion(1, bad, "yes"), suggestion(2, valued, "form"), suggestion(3, private, "form"), + suggestion(4, _suggested("Ok", "https://x.org/{station}"), "telepathy"), + {"id": 5, "kind": "mystery", "payload": {}}], sources=sources, probe=False, today="2026-09-28") + assert s["counts"] == {"rejected": 5} + assert not (sources / "suggested").exists() + + +def test_a_clashing_id_gets_a_suffix(sources): + a = _suggested("Temp", "https://api.harbordata.org/a/{station}") + b = _suggested("Temp", "https://api.harbordata.org/b/{station}") + s = apply([suggestion(1, a, "form"), suggestion(2, b, "form")], sources=sources, probe=False, + today="2026-09-28") + assert s["new"] == ["api-harbordata-org-temp", "api-harbordata-org-temp-2"] + + +def test_dry_run_writes_nothing(sources): + before = (sources / "curated" / "core.jsonl").read_text() + apply([vote(1, "noaa-tides", "yes"), suggestion(2, _suggested("T", "https://x.org/{station}"), "form")], + sources=sources, probe=False, write=False, today="2026-09-28") + assert (sources / "curated" / "core.jsonl").read_text() == before + assert not (sources / "suggested").exists() + + +def test_written_suggestions_pass_the_schema(sources): + from sourcetool.schema import validate_record + apply([suggestion(1, _suggested("Harbor", "https://api.harbordata.org/{station}"), "form")], sources=sources, + probe=False, today="2026-09-28") + for r in read_jsonl(sources / "suggested" / "2026-09-28.jsonl"): + assert validate_record(r) == [], json.dumps(r) From 84d2d1219b67d66e61520b81580fa4822b43e3e7 Mon Sep 17 00:00:00 2001 From: SecureCloudGroup Date: Mon, 28 Sep 2026 16:57:12 -0400 Subject: [PATCH 2/2] lookup: break exact score ties deterministically (keyless first, then prior, then id) Tied sources came back in arbitrary order, so the first source could differ between runs and between the Library and the app's port. Evals unchanged: dev 47/47, holdout 38/40, sealed 30/30. --- sourcetool/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sourcetool/build.py b/sourcetool/build.py index 5c6f145..7a15ec3 100644 --- a/sourcetool/build.py +++ b/sourcetool/build.py @@ -199,7 +199,7 @@ def _load_resolvers(con) -> int: FROM cand JOIN library_sources s ON s.id = cand.source_id LEFT JOIN hits h ON h.source_id = s.id LEFT JOIN catb c ON c.source_id = s.id WHERE s.validation_status NOT IN ('failed', 'refused') AND s.role <> 'helper' -ORDER BY score DESC LIMIT ?""" +ORDER BY score DESC, (s.auth <> 'none'), s.prior DESC, s.id LIMIT ?""" # resolvers that name a specific thing; places are too common a word-match to decide the source on their own.