feat(content): Sol body name pass and atlas tooling — complete #849 atlas cohesion
Finalizes #849 core-world atlas cohesion: GJ0d (Earth/Sol) markers.json cleaned of erroneous data, refine_log updated with Sol body gap notes, atlas_quality_analysis.py added for ongoing metric tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
# Atlas Generator Refinement Notes — Sprint 36
|
||||
|
||||
**Scope:** Systems-level sanity pass across 273 inhabited bodies (ticket #838).
|
||||
**Date:** 2026-04-19
|
||||
**Author:** Gestalt (systems)
|
||||
|
||||
This document records systematic generator artifacts found during the Sprint 36 atlas refinement pass. Each section describes the pattern, its severity, and the recommended generator patch.
|
||||
|
||||
---
|
||||
|
||||
## 1. Cross-Body City Name Collisions (SEVERE)
|
||||
|
||||
The Gemma naming pipeline exhausted its per-corridor vocabulary and defaulted to repeating high-probability names across bodies. 49 city names appear on more than one body; the worst offenders:
|
||||
|
||||
| Name | Bodies | Corridor |
|
||||
|---|---|---|
|
||||
| "Jade Harbor" | 20 | east_reach |
|
||||
| "Fort Iron" | 10 | deep_frontier |
|
||||
| "Forum Veritas" | 9 | core |
|
||||
| "Ridge Marker" | 8 | deep_frontier |
|
||||
| "Eisenstadt" | 7 | west_reach |
|
||||
| "Fjordheim" | 6 | west_reach |
|
||||
| "Fjordholm" | 6 | west_reach / north_reach |
|
||||
| "Ridge Line" | 6 | deep_frontier |
|
||||
| "Dusty Gully" | 5 | north_reach |
|
||||
| "Eisenberg" | 5 | west_reach |
|
||||
| "Eisenfels" | 5 | west_reach |
|
||||
| "Hanseong" | 5 | east_reach |
|
||||
|
||||
**Root cause:** The dedup set in `gemma_naming.py` tracks taken names per `(system_id, feature_type)` — only within a single system. Cross-system dedup does not exist. Bodies in different systems can receive identical names from the same high-probability tokens.
|
||||
|
||||
**Fix required:** Implement a global (or corridor-scoped) name registry that persists across system boundaries during batch runs. The `discover_bodies()` / `name_features_batch()` pipeline should seed the taken list from atlas_cities before processing each body, not just from within the current system.
|
||||
|
||||
**Resolution (Sprint 36):** Mellanie completed a full sweep eliminating all city collisions across 273 inhabited bodies (committed 48b73404). Clusters eliminated include Forum Veritas ×10, Jade Harbor ×19, Fort Iron ×10, Eisenstadt ×7, Fjordheim/Fjordholm ×6 each, Eisenberg/Eisenfels/Hanseong ×5 each, and 20+ smaller pairs. City collision count is now zero.
|
||||
|
||||
---
|
||||
|
||||
## 2. Cross-Body Mountain Name Collisions (SEVERE)
|
||||
|
||||
The same problem afflicts mountain ranges at a larger scale. From atlas_mountain_ranges (15,190 total features across all bodies):
|
||||
|
||||
| Name | Bodies |
|
||||
|---|---|
|
||||
| "" (empty/unnamed) | 186 bodies |
|
||||
| "Riverbend" | 39 bodies |
|
||||
| "Valley Floor" | 36 bodies |
|
||||
| "Steinbruch" | 26 bodies |
|
||||
| "Ridge Line" | 26 bodies |
|
||||
| "Ridge Crest" | 25 bodies |
|
||||
| "Gyeongju" | 24 bodies |
|
||||
| "Bamboo Grove" | 24 bodies |
|
||||
| "Zen Garden" | 23 bodies |
|
||||
| "River Bend" | 23 bodies |
|
||||
| "Ballynahown" | 23 bodies |
|
||||
| "Oakhaven" | 22 bodies |
|
||||
| "Feldberg" | 22 bodies |
|
||||
| "Rio Grande" | 21 bodies |
|
||||
| "Hanseong" | 21 bodies |
|
||||
|
||||
**186 empty-name mountain ranges** — the generator simply failed to produce a name for these features. They exist in the markers.json with `"name": ""`.
|
||||
|
||||
**Root cause:** Same as city collisions — no cross-system dedup. Additionally, mountain ranges are more numerous per body (avg ~8-12 per inhabited body) so the in-system pool depletes faster.
|
||||
|
||||
**Fix required:**
|
||||
1. Cross-system mountain name dedup (same approach as city fix above).
|
||||
2. Empty-name fallback logic: if Gemma returns an empty string or fails to generate a name, retry with a reduced temperature / different prompt pool entry, then fall back to a deterministic constructed name (`{body_name} Range {N}` is ugly but better than empty).
|
||||
|
||||
---
|
||||
|
||||
## 3. Mountain Suffix Monotony (MEDIUM)
|
||||
|
||||
On per-body passes, certain corridors show suffix clustering that makes mountain ranges feel templated rather than settled. The Vuurkloof (GJ35c) case was flagged in PR #130: 50% of mountains ended in `-rant` (Afrikaans for "edge/cliff"). This was a sampling artifact — the naming pipeline learned the pattern and reinforced it.
|
||||
|
||||
**Pattern:** When a corridor has a high-frequency suffix in its few-shot examples, Gemma completes with that suffix disproportionately. West_reach bodies show heavy `-berg` clustering; east_reach bodies show `-san` and `-yama` clustering.
|
||||
|
||||
**Fix required:** Post-generation suffix dedup — if >40% of a body's mountain names share the same trailing word/morpheme, re-query for the excess features with an explicit instruction to avoid that suffix.
|
||||
|
||||
---
|
||||
|
||||
## 4. Directional Compass Labels as Feature Names (MEDIUM)
|
||||
|
||||
Several template bodies (bodies with hand-authored names that the pipeline preserves) used pure directional compass labels for mountain ranges:
|
||||
|
||||
- Estrade (GJ280Ad): "Eastern Shelf", "Western Range", "Southern Heights" (all three mountains were compass labels)
|
||||
- Cairnside (GJ892d): "Westwall Range" (directional)
|
||||
|
||||
These convey no cultural or geographic character — they're the naming equivalent of "Mountain A, B, C."
|
||||
|
||||
**Fix applied (Sprint 36):** Estrade mountains renamed to Parallax Scarp, Vantage Ridge, Ledger Peaks. Cairnside "Westwall Range" renamed to Kappa Escarpment.
|
||||
|
||||
**Generator fix:** The Gemma few-shot pool entries in `_MOUNTAIN_POOLS` should explicitly include a negative example showing "Eastern Range / Northern Heights" as patterns to avoid, with a note: "Settlers name places after events, people, or features they see — not compass directions."
|
||||
|
||||
---
|
||||
|
||||
## 5. Zero Cross-Cultural Mixing on Corridor-Mismatched Bodies (MEDIUM)
|
||||
|
||||
Vuurkloof (GJ35c, south_reach corridor) was pure Afrikaans monoculture. The body's GTTR explicitly describes three centuries of Kumasi corridor influence and a transit-connected hospitality workforce, yet no Akan, Iberian, or Portuguese names existed in any feature category.
|
||||
|
||||
**Root cause:** The Gemma pipeline uses `cultural_corridor` to select naming palette (south_reach → Iberian/Portuguese) but the founding-culture context in the GTTR is not available to the naming model. When the founding culture and corridor palette diverge, the generator defaults to one or the other, not a blend.
|
||||
|
||||
**Fix applied (Sprint 36):** Vuurkloof mountains: Skerprant → Kwahu Scarp (Akan), Asrant → Crista das Cinzas (Portuguese), Waterrant → Bosomtwe Shelf (Akan). River: Rooistroom → Obotan (Akan). Ocean: Suidelike Poel → Lagoa do Sul (Portuguese).
|
||||
|
||||
**Generator fix:** The `gemma_naming.py` pipeline already reads `gttr_hook` per body. It should parse founding-culture cues from that hook and blend them with the corridor palette. A simple keyword detector for cultural markers (Afrikaans, Cape, Akan, Kumasi, etc.) could drive a `founding_culture_weight` that biases 30% of names toward founding-culture roots.
|
||||
|
||||
---
|
||||
|
||||
## 6. River Abstract/Navigational Naming (LOW-MEDIUM)
|
||||
|
||||
73 river names were flagged as abstract or navigational (using terms like "Flow", "Current", "Meridian", "Northern Flow"). Examples:
|
||||
|
||||
- "Delta Flow", "Northern Flow", "Celestial Flow" — generic
|
||||
- "The Meridian" — navigational abstraction
|
||||
- "Fogo Current", "Lagos Current", "M'Banza Current" — ocean-current framing applied to rivers
|
||||
|
||||
**Note:** "X Current" is appropriate for ocean surface currents; it reads oddly as a river name. Rivers should be named for features, people, or events, not for their direction of flow.
|
||||
|
||||
**Root cause:** The `_RIVER_POOLS` in `gemma_naming.py` include "current" and "flow" as acceptable completions, and some few-shot examples teach this pattern for certain corridors.
|
||||
|
||||
**Fix required:** Move "current" and "flow" suffix examples out of river pools and into ocean/sea pools only. Add a post-generation filter that flags river names ending in "Flow" or "Current" for re-query.
|
||||
|
||||
---
|
||||
|
||||
## 7. Coverage Gaps — Inhabited Bodies Missing Cities (AUDIT)
|
||||
|
||||
6 of 273 inhabited bodies have no cities in atlas_cities:
|
||||
|
||||
| Body | Name | Corridor | Class |
|
||||
|---|---|---|---|
|
||||
| GJ0d | Earth | sol-gateway-axis | temperate |
|
||||
| GJ0d-1 | Luna | sol-gateway-axis | barren |
|
||||
| GJ0e | Mars | sol-gateway-axis | arid |
|
||||
| GJ0f-2 | Europa | sol-gateway-axis | frozen |
|
||||
| GJ3522-belt | Pilbara Belt | core | — |
|
||||
| GJ820B-belt | — | core | — |
|
||||
|
||||
**Earth, Luna, Mars, Europa** — deferred to Paula's #849 core-world cohesion pass (Sol system, hop 0).
|
||||
|
||||
**Pilbara Belt, GJ820B-belt** — asteroid belts. These may not need traditional city placements. Recommend clarifying whether belt bodies should have mining stations marked as `kind: "outpost"` rather than cities, or be excluded from city generation entirely.
|
||||
|
||||
---
|
||||
|
||||
## 8. Road/Railroad Naming Gap (SEVERE — now fixed)
|
||||
|
||||
80% of all roads (37/46) and railroads (37/44) across inhabited bodies had empty names. The infrastructure geometry was generated correctly but the naming pipeline was never applied to road/railroad features — only to geographic features (cities, rivers, oceans, mountains).
|
||||
|
||||
**Fix applied (Sprint 36):** Named all 37 unnamed roads and 37 unnamed railroads using the city-pair convention: `{Capital}–{Destination} {corridor_suffix}` (corridor suffix: "Corridor" for core, "Road" for north_reach, "Estrada" for south_reach, "Strasse" for west_reach, "Track" for deep_frontier, "Express/Line" for railroads).
|
||||
|
||||
**Generator fix:** Extend the Gemma naming pipeline to include `roads` and `railroads` sections. Alternatively, a deterministic naming step from city pairs is sufficient — road names don't need cultural variation, just clarity.
|
||||
|
||||
---
|
||||
|
||||
## Metrics Before vs After Sprint 36 Refinement
|
||||
|
||||
| Metric | Before | After |
|
||||
|---|---|---|
|
||||
| Cross-body city collision names | 49 | 0 (Mellanie Sprint 36 full sweep) |
|
||||
| Worst collision ("Jade Harbor") | 20 bodies | 0 (eliminated) |
|
||||
| Unnamed roads | 37 / 46 (80%) | 0 / 46 (0%) |
|
||||
| Unnamed railroads | 37 / 44 (84%) | 0 / 44 (0%) |
|
||||
| Mountain cardinal-direction names (inhabited bodies) | ~45+ | 14 / 1638 (0%) |
|
||||
|
||||
---
|
||||
|
||||
## Fixes Applied This Sprint
|
||||
|
||||
### Pass 1 — Template bodies (PR #130 review)
|
||||
|
||||
| Body | Body Name | System | What Changed |
|
||||
|---|---|---|---|
|
||||
| GJ892d | Cairnside | GJ 892 (Cairnside) | "Westwall Range" → "Kappa Escarpment" |
|
||||
| GJ280Ad | Estrade | GJ 280A (Parallax) | "Eastern Shelf" → "Parallax Scarp"; "Western Range" → "Vantage Ridge"; "Southern Heights" → "Ledger Peaks" |
|
||||
| GJ35c | Vuurkloof | GJ 35 (Vuurkloof) | Mountains: Skerprant → Kwahu Scarp, Asrant → Crista das Cinzas, Waterrant → Bosomtwe Shelf; River: Rooistroom → Obotan; Ocean: Suidelike Poel → Lagoa do Sul |
|
||||
|
||||
### Pass 2 — Mid-tier bodies (severity-ranked pass)
|
||||
|
||||
| Body | Body Name | System | What Changed |
|
||||
|---|---|---|---|
|
||||
| GJ7547c | Brandwacht | Skemeraand | 6 cardinal mountains → Afrikaans names; city "Ridge Marker" → "Wagpos" |
|
||||
| GJ528Ac | Klaarstroom | Ouplaas | 4 cardinal mountains → Afrikaans names; city "Ridge Line" → "Klaardorp"; river "Riverbend" → "Die Draai" |
|
||||
| GJ68f | Winter | Schuilhoek | All 6 cardinal/navigational rivers renamed to Afrikaans; cities "Dust Bowl Flats"/"Barren Meadow" → "Stofkamp"/"Kaalveld" |
|
||||
| GJ68d | Lente | Schuilhoek | Wrong-type mountain names removed; 3 landscape-desc rivers → Afrikaans; 2 cap cities renamed |
|
||||
| GJ667Ad | Geelong | New Ballarat | 2 wrong-type mountain names → Anglo-Australian; 2 cap cities → Australian flora names |
|
||||
| GJ661Ad | Ys | Crown's Hollow | 2 collision city names → Anglo-Saxon unique names |
|
||||
| GJ15Ac | Gongshu | Lu Ban | Wrong-type mountain; 2 collision cities → institutional core names; 1 collision river |
|
||||
| GJ879d | Patiala | Singh's Landing | "Billabong" (water concept) + 6 cardinal mountains → Punjabi names; "Dusty Gully" → "Phillaur" |
|
||||
| GJ556c | Idanha | Recanto | 4 cardinal/wrong-type mountains → Portuguese names; cap city → "Miradouro" |
|
||||
| GJ138c | Portel | Sertão | Cap city → "Marco Sertão"; 4 concatenated river names → Portuguese |
|
||||
| GJ174c | Clausthal | Tiefenbach | Cardinal + wrong-type mountains → German names; cap city → "Bergstation"; 3 wrong-type rivers |
|
||||
| GJ421Bc | Serpa | Pedra Seca | 4 wrong-type mountains (flatland/valley floor used as mountains) → Portuguese names; cap city |
|
||||
| GJ566Ac | Haodu | Haodu | "Jade Harbor" (worst collision, 20 bodies) → "Lianyun Harbor" |
|
||||
| GJ674c | Provenance | Provenance | "Capitol Heights" → "Provenance Heights" |
|
||||
| GJ68c | Zomer | Schuilhoek | 2 collision city names → Afrikaans |
|
||||
|
||||
### Pass 2 — Infrastructure naming (all inhabited bodies)
|
||||
|
||||
All 37 unnamed roads and 37 unnamed railroads across 36+ inhabited bodies were named using the city-pair convention. Bodies touched: GJ71d, GJ144d, GJ144e, GJ725Bc, GJ166Ac, GJ251c, GJ3877c, GJ674c, GJ699b, GJ1286e, GJ15Ac, GJ447c, GJ768f, GJ783Ae, GJ1116Ac, GJ1289c, GJ273c, GJ3325d, GJ3622c, GJ411c, GJ475e, GJ566Ac, GJ667Ad, GJ667Bd, GJ68c, GJ68d, GJ68e, GJ68f, GJ680d, GJ75d, GJ877c, GJ879d, GJ1156d, GJ661Ad, GJ780e, GJ34Ad.
|
||||
|
||||
All bodies re-synced via `generate_atlas.py --body <id>` and verified in atlas_* tables.
|
||||
|
||||
---
|
||||
|
||||
## Deferred to Paula (#849)
|
||||
|
||||
- Edict (GJ244Ad / Sirius system): "Westwall" was not present in current markers.json or DB — either removed in a prior pass or the query data was stale. Paula's Sprint 36 pass renamed "Keel Ridge" → "Charter Spur" and "Sanction Ridge" → "The Statute". Edict mountains are clean. "Accord Peaks" cross-reference with Estrade's "Accord Run" river was evaluated and deemed acceptable (different feature types, no collision).
|
||||
- Sol system bodies: Earth, Luna, Mars, Europa — missing city placements, white-glove treatment needed.
|
||||
- Lendel (GJ380c / Groombridge system): check for any quality issues.
|
||||
|
||||
---
|
||||
|
||||
## 9. POI Audit — Sprint 36 (LOW severity)
|
||||
|
||||
**Scope:** `atlas_pois` and `atlas_body_grids` audited post-#838.
|
||||
|
||||
### atlas_body_grids
|
||||
Pure structural data (body_id, grid_w, grid_h, updated_at). No name column. **Clean — no action required.**
|
||||
|
||||
### atlas_pois
|
||||
287 total POIs across 267 inhabited bodies. Kind distribution: 267 transit (gate terminals), 15 institutional, 4 commercial, 1 corporate.
|
||||
|
||||
**Zero empty names.** All 267 transit POIs have names. Institutional/commercial/corporate POIs are all hand-authored (template bodies only) and clean.
|
||||
|
||||
**Cross-body duplicates (LOW):**
|
||||
|
||||
| Name | Bodies | Note |
|
||||
|---|---|---|
|
||||
| "North Fork" | 5 | Geographic feature name used as transit POI — reads as generic |
|
||||
| "Transit Hub" | 4 | Generator fallback — no locally grounded name derived |
|
||||
| "Shizuka Port" | 3 | east_reach name on 3 separate bodies |
|
||||
| "Ordnungshof" | 3 | west_reach name on 3 separate bodies |
|
||||
| 8 others | 2 each | Minor |
|
||||
|
||||
**Assessment:** Severity is LOW. Max collision depth is 5 bodies ("North Fork") vs. 20 for worst city collision. No empty names. The non-transit POIs (institutional/commercial/corporate) are entirely hand-authored and show no issues. Transit POIs are the only generator output category — most are correctly named "{Capital} Gate Terminal" or "{Body} Gate Terminal".
|
||||
|
||||
**No hand-fixes required this sprint.** The 4× "Transit Hub" entries are the only meaningful quality gap (generic fallback), but transit POIs are low-visibility in Phase 3 (Phase 1/2 priority).
|
||||
|
||||
**Generator fixes recommended (add to #853):**
|
||||
|
||||
7. **Transit POI deterministic naming** — derive gate terminal name from body's capital city: `{capital_name} Gate Terminal`. Current fallback to "Transit Hub" is a generator gap, same root cause as unnamed roads/railroads.
|
||||
8. **Cross-system POI dedup** — same approach as city/mountain dedup (global taken set per feature type).
|
||||
|
||||
---
|
||||
|
||||
## Generator Patches Required (Future Ticket)
|
||||
|
||||
Recommend creating a generator-patch ticket to address:
|
||||
|
||||
1. **Cross-system city name dedup** — seed taken list from global atlas_cities
|
||||
2. **Cross-system mountain name dedup** — same approach
|
||||
3. **Empty-name fallback** — retry logic + deterministic fallback when generation fails
|
||||
4. **Suffix monotony post-filter** — re-query if >40% same suffix per body
|
||||
5. **Founding culture blend** — parse gttr_hook for cultural cues, blend with corridor palette
|
||||
6. **River "Flow/Current" filter** — move these to ocean pools, post-gen filter on rivers
|
||||
7. **Transit POI deterministic naming** — derive from capital city name, eliminate "Transit Hub" fallback
|
||||
8. **Cross-system POI name dedup** — extend global dedup to atlas_pois
|
||||
9. **River/ocean cross-body name dedup (secondary/uninhabited bodies)** — Mellanie's Sprint 36 sweep confirmed river/ocean collisions remain on secondary and uninhabited bodies (Rio Grande ×23, Steinbruch ×19, others). Inhabited body rivers were addressed in passes 1–2; uninhabited body rivers require a separate scripted dedup pass. Same root cause as city/mountain: no global taken set in Gemma pipeline.
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
atlas_quality_analysis.py — Atlas content quality audit for Sprint 36 (#849/#838).
|
||||
|
||||
Queries atlas_* tables in systems.db and reports on:
|
||||
1. Cross-body name collisions (same name, same feature type, different bodies)
|
||||
2. Cardinal/directional name density per body
|
||||
3. Generic/lazy name patterns
|
||||
4. Earth-echo concentration in high-visibility systems
|
||||
5. Top-stem frequency across all named features
|
||||
|
||||
Usage:
|
||||
python3 tooling/planet-gen/atlas_quality_analysis.py [--db server/data/systems.db]
|
||||
python3 tooling/planet-gen/atlas_quality_analysis.py --system GJ380
|
||||
python3 tooling/planet-gen/atlas_quality_analysis.py --top-collisions 20
|
||||
python3 tooling/planet-gen/atlas_quality_analysis.py --body GJ71c
|
||||
|
||||
D-191 §8: markers.json is pixel-space [row, col] against 512×256.
|
||||
Re-run after any hand-refine pass to verify improvements.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sqlite3
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
DEFAULT_DB = REPO_ROOT / "server" / "data" / "systems.db"
|
||||
|
||||
CARDINAL_RE = re.compile(
|
||||
r"\b(north|south|east|west|eastern|western|northern|southern|"
|
||||
r"upper|lower|new|great|old|central|inner|outer|kita|minami|higashi|nishi)\b",
|
||||
re.I,
|
||||
)
|
||||
GENERIC_RE = re.compile(
|
||||
r"\b(hilly|sector|zone|district)\b"
|
||||
r"|^(great|the great|hilly)\b"
|
||||
r"|^(valley floor|ridge line|ridge crest|flat ground|riverbend)$",
|
||||
re.I,
|
||||
)
|
||||
EARTH_ECHO_RE = re.compile(
|
||||
r"\b(manchester|london|paris|berlin|tokyo|beijing|new york|sydney|dubai|"
|
||||
r"route \d+|sector \d+|block \d+)\b",
|
||||
re.I,
|
||||
)
|
||||
FEATURE_TABLES = [
|
||||
("atlas_cities", "city"),
|
||||
("atlas_rivers", "river"),
|
||||
("atlas_oceans", "ocean"),
|
||||
("atlas_mountain_ranges", "mountain"),
|
||||
]
|
||||
|
||||
|
||||
def open_db(path: str) -> sqlite3.Connection:
|
||||
return sqlite3.connect(path)
|
||||
|
||||
|
||||
def build_body_index(conn: sqlite3.Connection) -> dict:
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
"SELECT body_id, system_id, proper_name, cultural_corridor, population "
|
||||
"FROM bodies WHERE inhabited=1"
|
||||
)
|
||||
return {
|
||||
r[0]: {"system_id": r[1], "name": r[2], "corridor": r[3], "pop": r[4]}
|
||||
for r in c.fetchall()
|
||||
}
|
||||
|
||||
|
||||
def gather_all_names(conn: sqlite3.Connection) -> dict[str, list[tuple[str, str, str]]]:
|
||||
"""body_id → [(feature_type, name, local_id), ...]"""
|
||||
c = conn.cursor()
|
||||
result = defaultdict(list)
|
||||
for tbl, feat_type in FEATURE_TABLES:
|
||||
try:
|
||||
c.execute(f"SELECT body_id, name, local_id FROM {tbl} WHERE name IS NOT NULL AND name != ''")
|
||||
for body_id, name, local_id in c.fetchall():
|
||||
result[body_id].append((feat_type, name, local_id))
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def cross_body_collisions(conn: sqlite3.Connection, limit: int = 20) -> dict:
|
||||
c = conn.cursor()
|
||||
collisions = {}
|
||||
for tbl, feat_type in FEATURE_TABLES:
|
||||
try:
|
||||
c.execute(
|
||||
f"SELECT name, COUNT(DISTINCT body_id) as cnt, GROUP_CONCAT(DISTINCT body_id) "
|
||||
f"FROM {tbl} WHERE name IS NOT NULL AND name != '' "
|
||||
f"GROUP BY name HAVING cnt > 1 ORDER BY cnt DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
collisions[feat_type] = [(r[0], r[1], r[2]) for r in c.fetchall()]
|
||||
except sqlite3.OperationalError:
|
||||
collisions[feat_type] = []
|
||||
return collisions
|
||||
|
||||
|
||||
def stem_frequency(names: list[str], top_n: int = 30) -> list[tuple[str, int]]:
|
||||
stems = Counter()
|
||||
for name in names:
|
||||
words = name.split()
|
||||
if words:
|
||||
stems[words[0].lower()] += 1
|
||||
return stems.most_common(top_n)
|
||||
|
||||
|
||||
def body_quality_report(body_id: str, names: list[tuple], conn: sqlite3.Connection) -> dict:
|
||||
total = len(names)
|
||||
if total == 0:
|
||||
return {}
|
||||
cardinal = sum(1 for _, n, _ in names if CARDINAL_RE.search(n))
|
||||
generic = sum(1 for _, n, _ in names if GENERIC_RE.search(n))
|
||||
earth = sum(1 for _, n, _ in names if EARTH_ECHO_RE.search(n))
|
||||
|
||||
c = conn.cursor()
|
||||
# collision count: how many of this body's names appear on other bodies (same type)
|
||||
colliding = 0
|
||||
for feat_type, name, _ in names:
|
||||
tbl = [t for t, f in FEATURE_TABLES if f == feat_type][0]
|
||||
try:
|
||||
c.execute(
|
||||
f"SELECT COUNT(DISTINCT body_id) FROM {tbl} WHERE name=? AND body_id!=?",
|
||||
(name, body_id),
|
||||
)
|
||||
others = c.fetchone()[0]
|
||||
if others > 0:
|
||||
colliding += 1
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"cardinal": cardinal,
|
||||
"cardinal_pct": cardinal / total,
|
||||
"generic": generic,
|
||||
"earth_echo": earth,
|
||||
"colliding": colliding,
|
||||
"colliding_pct": colliding / total,
|
||||
}
|
||||
|
||||
|
||||
def run_analysis(args):
|
||||
conn = open_db(args.db)
|
||||
body_index = build_body_index(conn)
|
||||
all_names_by_body = gather_all_names(conn)
|
||||
|
||||
# Filter by system or body if requested
|
||||
if args.system:
|
||||
body_index = {k: v for k, v in body_index.items() if v["system_id"] == args.system}
|
||||
if args.body:
|
||||
body_index = {k: v for k, v in body_index.items() if k == args.body}
|
||||
|
||||
print("=" * 70)
|
||||
print("ATLAS QUALITY ANALYSIS — The Settled Reach (#849/#838)")
|
||||
print(f"DB: {args.db}")
|
||||
if args.system:
|
||||
print(f"Filter: system={args.system}")
|
||||
if args.body:
|
||||
print(f"Filter: body={args.body}")
|
||||
print("=" * 70)
|
||||
|
||||
# --- 1. Cross-body collisions ---
|
||||
print("\n[ 1. CROSS-BODY NAME COLLISIONS ]")
|
||||
collisions = cross_body_collisions(conn, limit=args.top_collisions)
|
||||
for feat_type, rows in collisions.items():
|
||||
if rows:
|
||||
print(f"\n {feat_type}:")
|
||||
for name, cnt, bodies in rows:
|
||||
print(f" '{name}' — {cnt} bodies: {bodies[:80]}")
|
||||
|
||||
# --- 2. Per-body quality scores ---
|
||||
print("\n[ 2. BODY QUALITY SCORES — ranked by collision % ]")
|
||||
reports = []
|
||||
for bid, info in body_index.items():
|
||||
names = all_names_by_body.get(bid, [])
|
||||
if not names:
|
||||
continue
|
||||
report = body_quality_report(bid, names, conn)
|
||||
if not report:
|
||||
continue
|
||||
reports.append((bid, info, report))
|
||||
|
||||
reports.sort(key=lambda x: -x[2]["colliding_pct"])
|
||||
|
||||
print(f"\n {'Body':<28} {'System':<12} {'Corridor':<15} "
|
||||
f"{'Coll%':>6} {'Card%':>6} {'Gen':>4} {'Echo':>4}")
|
||||
for bid, info, rep in reports[:30]:
|
||||
print(
|
||||
f" {(info['name'] or bid):<28} {info['system_id']:<12} {info['corridor'] or '?':<15} "
|
||||
f"{rep['colliding_pct']:>6.0%} {rep['cardinal_pct']:>6.0%} "
|
||||
f"{rep['generic']:>4} {rep['earth_echo']:>4}"
|
||||
)
|
||||
|
||||
# --- 3. Stem frequency ---
|
||||
print("\n[ 3. TOP STEM FREQUENCY (first word of name) ]")
|
||||
all_names_flat = [n for names in all_names_by_body.values() for _, n, _ in names]
|
||||
for stem, cnt in stem_frequency(all_names_flat, top_n=20):
|
||||
print(f" {stem:<20} {cnt}")
|
||||
|
||||
# --- 4. Detailed body report (if --body specified) ---
|
||||
if args.body and args.body in all_names_by_body:
|
||||
bid = args.body
|
||||
info = body_index.get(bid, {})
|
||||
names = all_names_by_body[bid]
|
||||
print(f"\n[ 4. DETAILED REPORT: {bid} ({info.get('name', '?')}) ]")
|
||||
c = conn.cursor()
|
||||
for feat_type, name, local_id in sorted(names, key=lambda x: x[0]):
|
||||
tbl = [t for t, f in FEATURE_TABLES if f == feat_type][0]
|
||||
c.execute(
|
||||
f"SELECT COUNT(DISTINCT body_id) FROM {tbl} WHERE name=? AND body_id!=?",
|
||||
(name, bid),
|
||||
)
|
||||
others = c.fetchone()[0]
|
||||
flag = f" *** COLLISION ×{others}" if others > 0 else ""
|
||||
cardinal = " [cardinal]" if CARDINAL_RE.search(name) else ""
|
||||
generic = " [generic]" if GENERIC_RE.search(name) else ""
|
||||
print(f" {feat_type:<10} {local_id:<12} {name}{flag}{cardinal}{generic}")
|
||||
|
||||
# --- 5. Sol gap check ---
|
||||
print("\n[ 5. SOL SYSTEM GAP CHECK ]")
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT body_id, proper_name, population FROM bodies WHERE system_id='GJ 0' AND inhabited=1")
|
||||
sol_bodies = c.fetchall()
|
||||
for bid, bname, pop in sol_bodies:
|
||||
has_cities = bid in all_names_by_body and any(f == "city" for f, _, _ in all_names_by_body[bid])
|
||||
status = "HAS DATA" if has_cities else "*** EMPTY — needs authoring"
|
||||
print(f" {bid:<15} {bname or '?':<20} pop={pop or '?'} {status}")
|
||||
|
||||
conn.close()
|
||||
print("\nDone.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--db", default=str(DEFAULT_DB), help="Path to systems.db")
|
||||
parser.add_argument("--system", help="Filter to one system (e.g. GJ380)")
|
||||
parser.add_argument("--body", help="Filter to one body (e.g. GJ71c)")
|
||||
parser.add_argument("--top-collisions", type=int, default=15, help="Collision list limit")
|
||||
args = parser.parse_args()
|
||||
run_analysis(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -234,7 +234,37 @@ in log and move on if already above quality bar."
|
||||
|
||||
#### GJ0d — Earth (8.5B pop, core world)
|
||||
|
||||
50 cities from earth_features.json (London, Tokyo, Delhi, Shanghai, etc.). sol_import.py
|
||||
sol_import.py originally placed 50 cities from earth_features.json. Team-lead directed trim
|
||||
to 8-12 cultural touchstones (max 8 cities on any other body; 50 = 13% of all atlas cities
|
||||
on one body). Criterion: would a player setting a bookmark to "Earth" recognize this as a
|
||||
touchstone? One per major historical/cultural cluster.
|
||||
|
||||
**Cities kept (11):**
|
||||
|
||||
| City | Cluster |
|
||||
|---|---|
|
||||
| London | Western Europe — historical capital |
|
||||
| Moscow | Eastern Europe / Russia |
|
||||
| Istanbul | Bridge city — Europe-Asia hinge |
|
||||
| New York | North America |
|
||||
| São Paulo | South America |
|
||||
| Cairo | Africa + ancient world |
|
||||
| Delhi | South Asia |
|
||||
| Tokyo | Japan / East Asia |
|
||||
| Beijing | China / East Asia |
|
||||
| Singapore | Southeast Asia — maritime hub |
|
||||
| Sydney | Oceania |
|
||||
|
||||
**Cities cut (39):** Paris, Berlin, Mexico City, Los Angeles, Toronto, Chicago, Lima, Bogotá,
|
||||
Rio de Janeiro, Buenos Aires, Lagos, Kinshasa, Johannesburg, Nairobi, Tehran, Baghdad, Riyadh,
|
||||
Ankara, Karachi, Shanghai, Mumbai, Jakarta, Dhaka, Manila, Bangkok, Seoul, Osaka, Chongqing,
|
||||
Kolkata, Lahore, Shenzhen, Bangalore, Ho Chi Minh City, Luanda, Addis Ababa, Santiago, Taipei,
|
||||
Hong Kong, Casablanca.
|
||||
|
||||
**Generator note:** sol_import.py has no `--top-n` city filter — it uses the full earth_features.json
|
||||
list. If Sol is regenerated, earth_features.json should be trimmed to the 11 kept cities, or a
|
||||
filter added in sol_import.py. Filed as finding in #853.
|
||||
|
||||
auto-detected 11 rivers and 1 ocean. Three rivers were auto-detected ocean-channel artifacts
|
||||
in the western Pacific island region; named with geographically proximate rivers. The large
|
||||
ocean (area_fraction=0.7049) represents Earth's interconnected world ocean.
|
||||
@@ -246,7 +276,7 @@ ocean (area_fraction=0.7049) represents Earth's interconnected world ocean.
|
||||
| river | null (river_4, ~27°N/136°E) | Tone River | Japan, Kanto plain |
|
||||
| river | null (river_10, ~23°N/135°E) | Cagayan | Northern Philippines, largest Philippine river |
|
||||
|
||||
**DB sync:** `generate_atlas.py --body GJ0d` ✓
|
||||
**DB sync:** `generate_atlas.py --body GJ0d` ✓ (11 cities, trimmed from 50)
|
||||
|
||||
---
|
||||
|
||||
@@ -316,20 +346,58 @@ Team-lead confirmed: defer this sprint. Finding documented for future pass.
|
||||
|
||||
---
|
||||
|
||||
## Audit metrics (before/after comparison)
|
||||
## Audit metrics — final confirmed delta
|
||||
|
||||
All audited systems post-fix:
|
||||
Final audit run: 2026-04-19. All 6 systems re-run after all edits. Results below are
|
||||
from `atlas_cohesion_audit.py` against `server/data/systems.db`.
|
||||
|
||||
| System | Empty names before | After | Lazy outputs before | After | Cardinals before | After | Key issues resolved |
|
||||
|--------|-------------------|----|--------------------|----|-----------------|-------|-----|
|
||||
| GJ 144 (Ran) | 0 | 0 | 10+ | 0 | 1 | 0 | Aldren/GJ380c cross-system collision fixed |
|
||||
| GJ 71 (Tau Ceti) | 0 | 0 | 2 | 0 | 0 | 0 | Concordia, Basilica Nova cross-body collisions |
|
||||
| GJ 244A (Sirius) | 0 | 0 | 2 | 0 | 3 | 0 | Westwall not present; Cairnside resolved |
|
||||
| GJ 380 (Groombridge) | 0 | 0 | 2 | 0 | 0 | 0 | Lazy suffixes on two #833 features |
|
||||
| GJ 699 (Barnard's) | 0 | 0 | 1 | 0 | 0 | 0 | Civic vocab on rivers/oceans; street addresses on moon |
|
||||
| GJ 0 (Sol) | 33 | 0 | 0 | 0 | 0 | 0 | All auto-detected null-name features named |
|
||||
### Inhabited body targets (in scope for this ticket)
|
||||
|
||||
**Total name edits across all bodies:** 23 (non-Sol) + 33 (Sol) = 56 total.
|
||||
| System | Body | Empty names: before→after | Lazy outputs: before→after | Cardinals: before→after |
|
||||
|--------|------|--------------------------|---------------------------|------------------------|
|
||||
| GJ 144 | GJ144d Kallast (2B) | 0→0 | 3→0 | 0→0 |
|
||||
| GJ 144 | GJ144e Vethis (1.2B) | 0→0 | 7→0 | 1→0 |
|
||||
| GJ 71 | GJ71c Threshold (600M) | 0→0 | 1→0 | 0→0 |
|
||||
| GJ 71 | GJ71d Arden (500M) | 0→0 | 1→0 | 0→0 |
|
||||
| GJ 71 | GJ71d-1 Verantis (20M) | 0→0 | 0→0 | 0→0 |
|
||||
| GJ 244A | GJ244Ad Edict (400M) | 0→0 | 2→0 | 3→0 |
|
||||
| GJ 380 | GJ380c Lendel (900M) | 0→0 | 2→0 | 0→0 |
|
||||
| GJ 699 | GJ699b Verada (1.9B) | 0→0 | 8→0 | 0→0 |
|
||||
| GJ 699 | GJ699b-1 (uninhabited moon) | 8→0 | 8→0 | 0→0 |
|
||||
| GJ 0 | GJ0d Earth (8.5B) | 4→0 | 0→0 | 0→0 |
|
||||
| GJ 0 | GJ0d-1 Luna (350M) | 24→0 | 0→0 | 0→0 |
|
||||
| GJ 0 | GJ0e Mars (1.2B) | 4→0 | 0→0 | 0→0 |
|
||||
| GJ 0 | GJ0f-2 Europa (30M) | 1→0 | 0→0 | 0→0 |
|
||||
|
||||
**All inhabited targets: zero empty names, zero lazy outputs, zero cardinals after fixes.**
|
||||
|
||||
### Remaining audit flags — out of scope or false positives
|
||||
|
||||
After fixes, the audit still reports flags on:
|
||||
|
||||
**Out of scope — uninhabited/low-pop bodies (not "high-visibility"):**
|
||||
- GJ144b, GJ144c, GJ144d-1, GJ144e-1, GJ144f, GJ144g-1, GJ144g-2: "Canyon View", "Dry Gulch",
|
||||
"Stone Creek" etc. These are #833 batch artifacts on non-target bodies. Captured in #853.
|
||||
- GJ71e: "Meridian Point" (uninhabited body, not in scope)
|
||||
- GJ380b, GJ380d, GJ380e: various lazy patterns (uninhabited, not in scope)
|
||||
|
||||
**False positives on quality cross-reference names (distinctive stem + common suffix):**
|
||||
- `'Rán's Run'` (GJ144d) — Rán- arc; % Run pattern-matched but stem is unique proper name
|
||||
- `'Greywash Fork'` (GJ144e) — Grey- arc; % Fork but Greywash is not a generic stem
|
||||
- `'Kelside Run'` (GJ144e) — Kel- arc; % Run but Kelside is distinctive
|
||||
- `'Greystone Ridge'` (GJ144e) — Grey- arc; % Ridge but Greystone is distinctive
|
||||
|
||||
These four are intentional renames (listed in the FIXES table above) that happen to end with
|
||||
a suffix in LAZY_PATTERNS. The script does not evaluate stem quality, only suffix pattern.
|
||||
A future pass on the audit script could add a stem-distinctiveness filter.
|
||||
|
||||
### Earth city count correction
|
||||
|
||||
Per team-lead direction: Earth trimmed from **50 → 11 cities** (cultural/historical touchstones,
|
||||
one per major cluster). 39 cities cut. DB synced. See GJ0d section above for full cut list.
|
||||
|
||||
**Total name edits across all bodies:** 23 (non-Sol) + 33 (Sol) = 56 feature renames.
|
||||
**Earth city cut:** 39 removed.
|
||||
|
||||
Intentional same-body cross-feature stem dups (quality arcs) now visible in audit output for:
|
||||
- GJ144d: Rán- (city + river), Seter- (mountain + ocean)
|
||||
@@ -378,7 +446,7 @@ SR_DB_PATH="$(pwd)/server/data/systems.db" tooling/db/sqlite-query \
|
||||
- `wiki/star-systems/GJ-699/bodies/GJ699b/markers.json` (8 edits)
|
||||
- `wiki/star-systems/GJ-699/bodies/GJ699b-1/markers.json` (8 edits)
|
||||
- `tooling/planet-gen/sol_name_fixes.py` (new — names 33 null-name Sol features)
|
||||
- `wiki/star-systems/GJ-0/bodies/GJ0d/markers.json` (1 ocean + 3 rivers named)
|
||||
- `wiki/star-systems/GJ-0/bodies/GJ0d/markers.json` (1 ocean + 3 rivers named; 39 cities cut → 11)
|
||||
- `wiki/star-systems/GJ-0/bodies/GJ0d-1/markers.json` (24 mountain ranges named)
|
||||
- `wiki/star-systems/GJ-0/bodies/GJ0e/markers.json` (4 mountain ranges named)
|
||||
- `wiki/star-systems/GJ-0/bodies/GJ0f-2/markers.json` (1 mountain range named)
|
||||
|
||||
@@ -863,33 +863,6 @@
|
||||
],
|
||||
"population": 12700000
|
||||
},
|
||||
{
|
||||
"id": "city_paris",
|
||||
"name": "Paris",
|
||||
"center": [
|
||||
80,
|
||||
261
|
||||
],
|
||||
"population": 11000000
|
||||
},
|
||||
{
|
||||
"id": "city_berlin",
|
||||
"name": "Berlin",
|
||||
"center": [
|
||||
77,
|
||||
269
|
||||
],
|
||||
"population": 3700000
|
||||
},
|
||||
{
|
||||
"id": "city_mexico_city",
|
||||
"name": "Mexico City",
|
||||
"center": [
|
||||
107,
|
||||
101
|
||||
],
|
||||
"population": 21800000
|
||||
},
|
||||
{
|
||||
"id": "city_new_york",
|
||||
"name": "New York",
|
||||
@@ -899,33 +872,6 @@
|
||||
],
|
||||
"population": 20100000
|
||||
},
|
||||
{
|
||||
"id": "city_los_angeles",
|
||||
"name": "Los Angeles",
|
||||
"center": [
|
||||
93,
|
||||
95
|
||||
],
|
||||
"population": 13200000
|
||||
},
|
||||
{
|
||||
"id": "city_toronto",
|
||||
"name": "Toronto",
|
||||
"center": [
|
||||
84,
|
||||
123
|
||||
],
|
||||
"population": 6200000
|
||||
},
|
||||
{
|
||||
"id": "city_chicago",
|
||||
"name": "Chicago",
|
||||
"center": [
|
||||
85,
|
||||
115
|
||||
],
|
||||
"population": 9500000
|
||||
},
|
||||
{
|
||||
"id": "city_s\u00e3o_paulo",
|
||||
"name": "S\u00e3o Paulo",
|
||||
@@ -935,60 +881,6 @@
|
||||
],
|
||||
"population": 22400000
|
||||
},
|
||||
{
|
||||
"id": "city_lima",
|
||||
"name": "Lima",
|
||||
"center": [
|
||||
133,
|
||||
131
|
||||
],
|
||||
"population": 10700000
|
||||
},
|
||||
{
|
||||
"id": "city_bogot\u00e1",
|
||||
"name": "Bogot\u00e1",
|
||||
"center": [
|
||||
121,
|
||||
135
|
||||
],
|
||||
"population": 11300000
|
||||
},
|
||||
{
|
||||
"id": "city_rio_de_janeiro",
|
||||
"name": "Rio de Janeiro",
|
||||
"center": [
|
||||
142,
|
||||
168
|
||||
],
|
||||
"population": 13500000
|
||||
},
|
||||
{
|
||||
"id": "city_buenos_aires",
|
||||
"name": "Buenos Aires",
|
||||
"center": [
|
||||
151,
|
||||
153
|
||||
],
|
||||
"population": 15200000
|
||||
},
|
||||
{
|
||||
"id": "city_lagos",
|
||||
"name": "Lagos",
|
||||
"center": [
|
||||
120,
|
||||
262
|
||||
],
|
||||
"population": 15400000
|
||||
},
|
||||
{
|
||||
"id": "city_kinshasa",
|
||||
"name": "Kinshasa",
|
||||
"center": [
|
||||
124,
|
||||
270
|
||||
],
|
||||
"population": 15600000
|
||||
},
|
||||
{
|
||||
"id": "city_cairo",
|
||||
"name": "Cairo",
|
||||
@@ -998,69 +890,6 @@
|
||||
],
|
||||
"population": 21300000
|
||||
},
|
||||
{
|
||||
"id": "city_johannesburg",
|
||||
"name": "Johannesburg",
|
||||
"center": [
|
||||
156,
|
||||
279
|
||||
],
|
||||
"population": 6000000
|
||||
},
|
||||
{
|
||||
"id": "city_nairobi",
|
||||
"name": "Nairobi",
|
||||
"center": [
|
||||
128,
|
||||
293
|
||||
],
|
||||
"population": 5100000
|
||||
},
|
||||
{
|
||||
"id": "city_tehran",
|
||||
"name": "Tehran",
|
||||
"center": [
|
||||
92,
|
||||
308
|
||||
],
|
||||
"population": 9000000
|
||||
},
|
||||
{
|
||||
"id": "city_baghdad",
|
||||
"name": "Baghdad",
|
||||
"center": [
|
||||
94,
|
||||
303
|
||||
],
|
||||
"population": 8100000
|
||||
},
|
||||
{
|
||||
"id": "city_riyadh",
|
||||
"name": "Riyadh",
|
||||
"center": [
|
||||
103,
|
||||
304
|
||||
],
|
||||
"population": 7700000
|
||||
},
|
||||
{
|
||||
"id": "city_ankara",
|
||||
"name": "Ankara",
|
||||
"center": [
|
||||
87,
|
||||
284
|
||||
],
|
||||
"population": 5700000
|
||||
},
|
||||
{
|
||||
"id": "city_karachi",
|
||||
"name": "Karachi",
|
||||
"center": [
|
||||
103,
|
||||
327
|
||||
],
|
||||
"population": 16500000
|
||||
},
|
||||
{
|
||||
"id": "city_tokyo",
|
||||
"name": "Tokyo",
|
||||
@@ -1079,15 +908,6 @@
|
||||
],
|
||||
"population": 32900000
|
||||
},
|
||||
{
|
||||
"id": "city_shanghai",
|
||||
"name": "Shanghai",
|
||||
"center": [
|
||||
97,
|
||||
387
|
||||
],
|
||||
"population": 28500000
|
||||
},
|
||||
{
|
||||
"id": "city_beijing",
|
||||
"name": "Beijing",
|
||||
@@ -1097,168 +917,6 @@
|
||||
],
|
||||
"population": 21500000
|
||||
},
|
||||
{
|
||||
"id": "city_mumbai",
|
||||
"name": "Mumbai",
|
||||
"center": [
|
||||
107,
|
||||
333
|
||||
],
|
||||
"population": 21700000
|
||||
},
|
||||
{
|
||||
"id": "city_jakarta",
|
||||
"name": "Jakarta",
|
||||
"center": [
|
||||
120,
|
||||
374
|
||||
],
|
||||
"population": 34500000
|
||||
},
|
||||
{
|
||||
"id": "city_dhaka",
|
||||
"name": "Dhaka",
|
||||
"center": [
|
||||
103,
|
||||
351
|
||||
],
|
||||
"population": 23000000
|
||||
},
|
||||
{
|
||||
"id": "city_manila",
|
||||
"name": "Manila",
|
||||
"center": [
|
||||
109,
|
||||
388
|
||||
],
|
||||
"population": 14400000
|
||||
},
|
||||
{
|
||||
"id": "city_bangkok",
|
||||
"name": "Bangkok",
|
||||
"center": [
|
||||
109,
|
||||
370
|
||||
],
|
||||
"population": 11000000
|
||||
},
|
||||
{
|
||||
"id": "city_seoul",
|
||||
"name": "Seoul",
|
||||
"center": [
|
||||
90,
|
||||
393
|
||||
],
|
||||
"population": 9800000
|
||||
},
|
||||
{
|
||||
"id": "city_osaka",
|
||||
"name": "Osaka",
|
||||
"center": [
|
||||
93,
|
||||
398
|
||||
],
|
||||
"population": 19300000
|
||||
},
|
||||
{
|
||||
"id": "city_chongqing",
|
||||
"name": "Chongqing",
|
||||
"center": [
|
||||
97,
|
||||
375
|
||||
],
|
||||
"population": 17000000
|
||||
},
|
||||
{
|
||||
"id": "city_kolkata",
|
||||
"name": "Kolkata",
|
||||
"center": [
|
||||
103,
|
||||
349
|
||||
],
|
||||
"population": 15100000
|
||||
},
|
||||
{
|
||||
"id": "city_lahore",
|
||||
"name": "Lahore",
|
||||
"center": [
|
||||
97,
|
||||
336
|
||||
],
|
||||
"population": 14000000
|
||||
},
|
||||
{
|
||||
"id": "city_shenzhen",
|
||||
"name": "Shenzhen",
|
||||
"center": [
|
||||
104,
|
||||
382
|
||||
],
|
||||
"population": 13400000
|
||||
},
|
||||
{
|
||||
"id": "city_bangalore",
|
||||
"name": "Bangalore",
|
||||
"center": [
|
||||
111,
|
||||
339
|
||||
],
|
||||
"population": 13200000
|
||||
},
|
||||
{
|
||||
"id": "city_ho_chi_minh_city",
|
||||
"name": "Ho Chi Minh City",
|
||||
"center": [
|
||||
113,
|
||||
374
|
||||
],
|
||||
"population": 9300000
|
||||
},
|
||||
{
|
||||
"id": "city_luanda",
|
||||
"name": "Luanda",
|
||||
"center": [
|
||||
132,
|
||||
268
|
||||
],
|
||||
"population": 9000000
|
||||
},
|
||||
{
|
||||
"id": "city_addis_ababa",
|
||||
"name": "Addis Ababa",
|
||||
"center": [
|
||||
119,
|
||||
292
|
||||
],
|
||||
"population": 5500000
|
||||
},
|
||||
{
|
||||
"id": "city_santiago",
|
||||
"name": "Santiago",
|
||||
"center": [
|
||||
147,
|
||||
137
|
||||
],
|
||||
"population": 7000000
|
||||
},
|
||||
{
|
||||
"id": "city_taipei",
|
||||
"name": "Taipei",
|
||||
"center": [
|
||||
103,
|
||||
388
|
||||
],
|
||||
"population": 7000000
|
||||
},
|
||||
{
|
||||
"id": "city_hong_kong",
|
||||
"name": "Hong Kong",
|
||||
"center": [
|
||||
104,
|
||||
382
|
||||
],
|
||||
"population": 7500000
|
||||
},
|
||||
{
|
||||
"id": "city_singapore",
|
||||
"name": "Singapore",
|
||||
@@ -1276,15 +934,6 @@
|
||||
421
|
||||
],
|
||||
"population": 5300000
|
||||
},
|
||||
{
|
||||
"id": "city_casablanca",
|
||||
"name": "Casablanca",
|
||||
"center": [
|
||||
93,
|
||||
249
|
||||
],
|
||||
"population": 3800000
|
||||
}
|
||||
],
|
||||
"railroads": [],
|
||||
|
||||
Reference in New Issue
Block a user