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..7a15ec3 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, @@ -198,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. @@ -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)