"""Flamingo Park nach OpenStreetMap in die extrahierte Welt einsetzen, dazu die Jefferson Avenue. Spec: docs/superpowers/specs/2026-09-15-flamingo-park-design.md Welt-x und Welt-y in Metern wie auf der Minimap; echte Punkte in flamingo-park.toml als Breite/Länge. Ein Formfehler in der Parkdatei bricht mit ParkError ab, wie bei annotations.toml. """ from __future__ import annotations import json import math import tomllib from dataclasses import dataclass, field, replace from pathlib import Path from shapely.geometry import LineString, Point, Polygon from shapely.ops import unary_union from .heights import PALM_CLEARANCE, PALM_KERB, palm_points class ParkError(ValueError): """Formfehler in der Parkdatei oder eine Welt, zu der sie nicht mehr passt: der Lauf bricht ab.""" # --- Parkdatei --------------------------------------------------------------------------------- @dataclass(frozen=True) class Anchor: name: str real: tuple[float, float] # Breite, Länge game: tuple[float, float] # Welt-x, Welt-y @dataclass(frozen=True) class Keepout: name: str at: tuple[float, float] rotation: float # Grad gegen den Uhrzeigersinn, wie PLACES bounds: tuple[float, float, float, float] # min_x, max_x, min_z, max_z in Modellkoordinaten @dataclass(frozen=True) class Street: start: tuple[float, float] end: tuple[float, float] width: float remove_buildings: tuple[tuple[float, float], ...] alley_cut: tuple[tuple[float, float], ...] @dataclass(frozen=True) class ParkConfig: osm_path: Path real: dict[str, tuple[float, float]] # sw, se, nw als Breite, Länge game: dict[str, tuple[float, float]] # sw, se, ne, nw als Welt-x, Welt-y scale_u: float offset_u: float anchors: tuple[Anchor, ...] keepouts: tuple[Keepout, ...] main_width: float path_width: float service_width: float default_height: float street: Street def load_park(path: Path) -> ParkConfig: if not path.exists(): raise ParkError(f"{path.name} fehlt") return parse_park(path.read_text(encoding="utf-8"), path.parent, path.name) def parse_park(text: str, base: Path, name: str = "flamingo-park.toml") -> ParkConfig: def fail(message: str): raise ParkError(f"{name}: {message}") try: data = tomllib.loads(text) except tomllib.TOMLDecodeError as err: fail(f"kein gültiges TOML: {err}") root = _Table(data, "", fail) root.only("osm", "fit", "anchor", "keepout", "paths", "buildings", "street") fit = root.table("fit") fit.only("real", "game", "scale_u", "offset_u") real, game = fit.table("real"), fit.table("game") real.only("sw", "se", "nw") game.only("sw", "se", "ne", "nw") anchors = [] for entry in root.tables("anchor"): entry.only("name", "real", "game") anchors.append(Anchor(entry.text("name"), entry.point("real"), entry.point("game"))) keepouts = [] for entry in root.tables("keepout"): entry.only("name", "at", "rotation", "bounds") bounds = entry.table("bounds") bounds.only("min_x", "max_x", "min_z", "max_z") box = tuple(bounds.number(key) for key in ("min_x", "max_x", "min_z", "max_z")) if box[0] >= box[1] or box[2] >= box[3]: fail(f"{entry.path}.bounds: min muss kleiner als max sein, ist {box!r}") keepouts.append(Keepout(entry.text("name"), entry.point("at"), entry.number("rotation"), box)) paths = root.table("paths") paths.only("main_width", "width", "service_width") buildings = root.table("buildings") buildings.only("default_height") street = root.table("street") street.only("from", "to", "width", "remove_buildings", "alley_cut") cut = street.points("alley_cut", 3) if not Polygon(cut).is_valid: fail("street.alley_cut muss ein Vieleck ohne sich schneidende Kanten sein") osm = root.text("osm") if not (base / osm).exists(): fail(f"OSM-Datei {osm} fehlt: cd tools/extract && uv run python osm.py") return ParkConfig( osm_path=base / osm, real={key: real.point(key) for key in ("sw", "se", "nw")}, game={key: game.point(key) for key in ("sw", "se", "ne", "nw")}, scale_u=fit.number("scale_u", positive=True), offset_u=fit.number("offset_u"), anchors=tuple(anchors), keepouts=tuple(keepouts), main_width=paths.number("main_width", positive=True), path_width=paths.number("width", positive=True), service_width=paths.number("service_width", positive=True), default_height=buildings.number("default_height", positive=True), street=Street(street.point("from"), street.point("to"), street.number("width", positive=True), street.points("remove_buildings", 0), cut), ) class _Table: """Eine TOML-Tabelle mit Pfad für Meldungen; jede Abfrage prüft Vorhandensein und Typ.""" def __init__(self, data, path: str, fail): if not isinstance(data, dict): fail(f"{path or 'Datei'} muss eine Tabelle sein") self.data, self.path, self.fail = data, path, fail def _key(self, key: str) -> str: return f"{self.path}.{key}" if self.path else key def only(self, *keys: str) -> None: for key in self.data: if key not in keys: self.fail(f"Feld {self._key(key)!r} gibt es nicht") def get(self, key: str): if key not in self.data: self.fail(f"Feld {self._key(key)!r} fehlt") return self.data[key] def table(self, key: str) -> _Table: return _Table(self.get(key), self._key(key), self.fail) def tables(self, key: str) -> list[_Table]: value = self.get(key) if not isinstance(value, list): self.fail(f"{self._key(key)} muss als [[{key}]] geschrieben sein") return [_Table(item, f"{self._key(key)}[{i}]", self.fail) for i, item in enumerate(value, start=1)] def text(self, key: str) -> str: value = self.get(key) if not isinstance(value, str) or not value.strip(): self.fail(f"Feld {self._key(key)!r} muss Text sein, ist {value!r}") return value def number(self, key: str, positive: bool = False) -> float: value = self.get(key) if not _is_number(value) or (positive and value <= 0): bound = " größer als 0" if positive else "" self.fail(f"Feld {self._key(key)!r} muss eine Zahl{bound} sein, ist {value!r}") return float(value) def point(self, key: str) -> tuple[float, float]: value = self.get(key) if not _is_point(value): self.fail(f"Feld {self._key(key)!r} muss ein Punkt [a, b] sein, ist {value!r}") return float(value[0]), float(value[1]) def points(self, key: str, minimum: int) -> tuple[tuple[float, float], ...]: value = self.get(key) if not (isinstance(value, list) and len(value) >= minimum and all(_is_point(p) for p in value)): self.fail(f"Feld {self._key(key)!r} muss mindestens {minimum} Punkte [a, b] enthalten, ist {value!r}") return tuple((float(a), float(b)) for a, b in value) def _is_number(value) -> bool: return isinstance(value, (int, float)) and not isinstance(value, bool) def _is_point(value) -> bool: return isinstance(value, list) and len(value) == 2 and all(_is_number(c) for c in value) # --- Einpassung -------------------------------------------------------------------------------- REF_LAT, REF_LON = 25.7840, -80.1375 # Bezugspunkt der lokalen Meter (Spec, Befund) M_PER_DEG_LAT = 111_320.0 M_PER_DEG_LON = M_PER_DEG_LAT * math.cos(math.radians(REF_LAT)) def local_metres(lat: float, lon: float) -> tuple[float, float]: """Breite/Länge -> Meter östlich und nördlich des Bezugspunkts.""" return (lon - REF_LON) * M_PER_DEG_LON, (lat - REF_LAT) * M_PER_DEG_LAT class ParkFit: """Echter Park -> Spielpark, affin über die Ecken SW, SE und NW (Variante D). Bilinear über alle vier Ecken folgte die Anlage der steileren Nordkante im Spiel und verfehlte den Hood Yard um 4,2 m; affin bleibt sie im Nordosten bis zu 6,4 m vor der Nordstraße, der Rasen füllt das ganze Parkpolygon trotzdem. """ def __init__(self, config): self.origin = local_metres(*config.real["sw"]) se, nw = local_metres(*config.real["se"]), local_metres(*config.real["nw"]) self.real_u = (se[0] - self.origin[0], se[1] - self.origin[1]) self.real_v = (nw[0] - self.origin[0], nw[1] - self.origin[1]) game = config.game self.game_origin = game["sw"] self.game_u = (game["se"][0] - game["sw"][0], game["se"][1] - game["sw"][1]) self.game_v = (game["nw"][0] - game["sw"][0], game["nw"][1] - game["sw"][1]) self.real_width = math.hypot(*self.real_u) self.game_width = math.hypot(*self.game_u) self.scale_u, self.offset_u = config.scale_u, config.offset_u self.game_quad = Polygon([game["sw"], game["se"], game["ne"], game["nw"]]) def park_coords(self, x: float, y: float) -> tuple[float, float]: """Lokale Meter -> (s, t): s von West nach Ost, t von Süd nach Nord, das echte Parkviereck ist [0, 1]².""" dx, dy = x - self.origin[0], y - self.origin[1] (ux, uy), (vx, vy) = self.real_u, self.real_v det = ux * vy - uy * vx return (dx * vy - dy * vx) / det, (ux * dy - uy * dx) / det def game_point(self, s: float, t: float) -> tuple[float, float]: u = (self.offset_u + self.scale_u * s * self.real_width) / self.game_width return (self.game_origin[0] + u * self.game_u[0] + t * self.game_v[0], self.game_origin[1] + u * self.game_u[1] + t * self.game_v[1]) def local_to_game(self, x: float, y: float) -> tuple[float, float]: return self.game_point(*self.park_coords(x, y)) def to_game(self, lat: float, lon: float) -> tuple[float, float]: return self.local_to_game(*local_metres(lat, lon)) def real_quad(self) -> Polygon: """Das echte Parkviereck in lokalen Metern.""" (ox, oy), (ux, uy), (vx, vy) = self.origin, self.real_u, self.real_v return Polygon([(ox, oy), (ox + ux, oy + uy), (ox + ux + vx, oy + uy + vy), (ox + vx, oy + vy)]) # --- Anlage aus OpenStreetMap ------------------------------------------------------------------ FIELD_SPORTS = {"baseball", "soccer"} COURT_SPORTS = {"tennis", "basketball", "racquet"} PATH_HIGHWAYS = {"footway", "path", "pedestrian", "steps", "cycleway"} MAIN_PATHS = {"Flamingo Park Path (13th St)", "Flamingo Park Path (Jefferson Ave)"} SURFACE_ORDER = ("field", "court", "pool", "parking", "sand") EDGE_REACH = 5.0 # m: so nah am echten West- oder Ostrand endende Wege laufen bis zur Straßenkante EXTEND_MAX = 90.0 # m: der Rasenstreifen im Osten ist 37 m breit, schräg ankommende Wege brauchen bis zu doppelt so viel @dataclass class ParkLayout: lawn: Polygon # das ganze Parkpolygon im Spiel surfaces: list[tuple[Polygon, str]] # Felder, Plätze, Pool, Parkplatz, Spielplatz, ohne Rasen paths: list[Polygon] service: list[Polygon] buildings: list[tuple[Polygon, float]] keepouts: list[Polygon] warnings: list[str] = field(default_factory=list) def load_elements(path: Path) -> list[dict]: """Die Elemente des OSM-Abzugs, so streng geprüft wie die Parkdatei. osm.py wird von Hand aufgerufen, der Abzug liegt also beliebig lange herum und kann abgeschnitten oder aus einer älteren Fassung des Skripts sein. Ohne diese Prüfung fällt das erst als nackter KeyError mitten in build_layout() auf — ohne Dateinamen und ohne den Hinweis, wie man den Abzug erneuert. """ def fail(message: str): raise ParkError(f"{path.name}: {message}") try: data = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as err: fail(f"kein gültiges JSON: {err}") elements = data.get("elements") if isinstance(data, dict) else None if not isinstance(elements, list): fail("Feld 'elements' fehlt oder ist keine Liste: cd tools/extract && uv run python osm.py") for n, element in enumerate(elements, start=1): where = f"elements[{n}]" if not isinstance(element, dict) else f"elements[{n}] (id {element.get('id')})" if not isinstance(element, dict) or not isinstance(element.get("tags"), dict): fail(f"{where} braucht eine Tabelle 'tags'") geometry = element.get("geometry") if not (isinstance(geometry, list) and all(_is_point(point) for point in geometry)): fail(f"{where} braucht 'geometry' als Liste von Punkten [Breite, Länge]") return elements def keepout_polygon(keepout: Keepout) -> Polygon: """Außenmaße eines Orts in Weltkoordinaten, gedreht wie modelToWorld in tests/placesSite.test.js.""" r = math.radians(keepout.rotation) cos, sin = math.cos(r), math.sin(r) min_x, max_x, min_z, max_z = keepout.bounds ax, ay = keepout.at corners = [(min_x, min_z), (max_x, min_z), (max_x, max_z), (min_x, max_z)] return Polygon([(ax + x * cos + z * sin, ay + x * sin - z * cos) for x, z in corners]) def building_height(tags: dict, default: float) -> float | None: """Höhe aus OSM, sonst default; None für Dächer ohne Wände (height = 0).""" raw = tags.get("height") if raw is None: return default try: height = float(str(raw).split()[0]) except (ValueError, IndexError): return default return None if height <= 0 else height def build_layout(elements: list[dict], config, fit: ParkFit) -> ParkLayout: real_quad = fit.real_quad() keepouts = [keepout_polygon(k) for k in config.keepouts] blocked = unary_union(keepouts) surfaces = {kind: [] for kind in SURFACE_ORDER} footways, service, buildings = [], [], [] for element in elements: tags, coords = element["tags"], element["geometry"] highway = tags.get("highway") if highway in PATH_HIGHWAYS or highway == "service": if len(coords) < 2: continue width = (config.service_width if highway == "service" else config.main_width if tags.get("name") in MAIN_PATHS else config.path_width) real_line = LineString([local_metres(lat, lon) for lat, lon in coords]) for piece in _pieces(real_line.intersection(real_quad)): if piece.geom_type != "LineString" or piece.length == 0: continue game = _extend(fit, piece, [fit.local_to_game(x, y) for x, y in piece.coords]) (service if highway == "service" else footways).append(LineString(game).buffer(width / 2)) continue if len(coords) < 4: continue real_poly = Polygon([local_metres(lat, lon) for lat, lon in coords]) if not real_poly.is_valid or not real_quad.contains(real_poly.centroid): continue game_poly = Polygon([fit.local_to_game(x, y) for x, y in real_poly.exterior.coords]) if "building" in tags: height = building_height(tags, config.default_height) if height is not None and not game_poly.intersects(blocked): buildings.append((game_poly, height)) continue kind = _surface_kind(tags) if kind is not None: surfaces[kind].extend(_polygons(game_poly.difference(blocked))) warnings, paths, service_polys = [], [], [] if footways: union = unary_union(footways) warnings += _keepout_warnings(union, config.keepouts, keepouts, "Parkweg") # Am Parkviereck abschneiden: _extend() legt den letzten Stützpunkt genau auf die Kante, # die runde Kappe von LineString.buffer() setzt danach bis zu eine halbe Wegbreite (3 m) # darüber hinaus. Gemessen lagen so 95,96 m² Weg außerhalb des Vierecks, davon rund 41 m² # als Betonzungen sichtbar auf den umgebenden Belägen. Die gewollte Reichweite bis zur # Straßenkante geht dabei nicht verloren, weil _extend() ohnehin auf diese Kante zielt. paths = _polygons(union.difference(blocked).intersection(fit.game_quad)) if service: service_union = unary_union(service) warnings += _keepout_warnings(service_union, config.keepouts, keepouts, "Zufahrt") service_polys = _polygons(service_union.difference(blocked)) return ParkLayout( lawn=fit.game_quad, surfaces=[(poly, kind) for kind in SURFACE_ORDER for poly in surfaces[kind]], paths=paths, service=service_polys, buildings=buildings, keepouts=keepouts, warnings=warnings, ) def _keepout_warnings(union, config_keepouts, keepout_polys, label: str) -> list[str]: """Warnungstexte für alle Sperrflächen, die `union` (Parkwege oder Zufahrten) schneiden.""" return [f"{label} schneidet die Sperrfläche {k.name}" for k, polygon in zip(config_keepouts, keepout_polys) if union.intersects(polygon)] def _surface_kind(tags: dict) -> str | None: sports = set(tags.get("sport", "").split(";")) leisure = tags.get("leisure") if leisure == "pitch": return "field" if sports & FIELD_SPORTS else "court" if sports & COURT_SPORTS else None if leisure == "sports_centre": return "court" if leisure == "swimming_pool": return "pool" if leisure == "playground": return "sand" if tags.get("amenity") == "parking": return "parking" return None def _extend(fit: ParkFit, real_line: LineString, coords: list) -> list: """Enden höchstens EDGE_REACH vor dem echten West- oder Ostrand laufen nach außen bis zur Straßenkante.""" coords = list(coords) ux, uy = fit.game_u[0] / fit.game_width, fit.game_u[1] / fit.game_width for end, inner in ((0, 1), (-1, -2)): if len(coords) < 2: return coords s, _ = fit.park_coords(*real_line.coords[end]) west, east = s * fit.real_width, (1 - s) * fit.real_width side = -1 if west <= EDGE_REACH else 1 if east <= EDGE_REACH else 0 if side == 0: continue (x0, y0), (x1, y1) = coords[inner], coords[end] length = math.hypot(x1 - x0, y1 - y0) if length == 0: continue dx, dy = (x1 - x0) / length, (y1 - y0) / length if (dx * ux + dy * uy) * side < 0.5: # läuft längs der Kante statt auf sie zu continue ray = LineString([(x1, y1), (x1 + dx * EXTEND_MAX, y1 + dy * EXTEND_MAX)]) hits = _points(ray.intersection(fit.game_quad.exterior)) if hits: coords[end] = min(hits, key=lambda p: math.hypot(p[0] - x1, p[1] - y1)) return coords def _pieces(geometry) -> list: if geometry.is_empty: return [] return list(getattr(geometry, "geoms", [geometry])) def _polygons(geometry, min_area: float = 1.0) -> list[Polygon]: return [g for g in _pieces(geometry) if g.geom_type == "Polygon" and g.area >= min_area] def _points(geometry) -> list[tuple[float, float]]: out = [] for g in _pieces(geometry): if g.geom_type == "Point": out.append((g.x, g.y)) elif g.geom_type in ("LineString", "LinearRing"): out.extend(g.coords) return out # --- Welt verändern ---------------------------------------------------------------------------- MIN_CENTERLINE = 15.0 # m, wie centerlines() in vectors.py: kürzere Stummel setzten Laternen auf die Kreuzung MIN_ROAD_AREA = 20.0 # m², wie road_polygons() in vectors.py @dataclass class World: """Weltgeometrie zwischen Extraktion und Chunk-Ausgabe, in Welt-x/Welt-y.""" land: list[Polygon] surfaces: list[tuple[Polygon, str]] roads: list[tuple[Polygon, bool]] centerlines: list[LineString] buildings: list[tuple[Polygon, dict, bool]] # Grundriss, {"height", "source"}, spekulativ palms: list[tuple[float, float]] paths: list[Polygon] = field(default_factory=list) def _lines(geometry, min_length: float = MIN_CENTERLINE) -> list[LineString]: return [g for g in _pieces(geometry) if g.geom_type == "LineString" and g.length >= min_length] def apply_street(world: World, street: Street) -> World: """Gebäude und Gasse weg, gerade Straße mit Mittellinie, frei gewordene Fläche wird Stadtfläche.""" cut = Polygon(street.alley_cut) def hit(building) -> bool: return any(building[0].contains(Point(p)) for p in street.remove_buildings) removed = [b[0] for b in world.buildings if hit(b)] buildings = [b for b in world.buildings if not hit(b)] line = LineString([street.start, street.end]) corridor = line.buffer(street.width / 2, cap_style="flat") kept, joined = [], [corridor] for poly, bridge in world.roads: rest = poly.difference(cut) if poly.intersects(cut) else poly for piece in _polygons(rest, MIN_ROAD_AREA): if not bridge and piece.intersects(corridor): joined.append(piece) else: kept.append((piece, bridge)) roads = kept + [(piece, False) for piece in _polygons(unary_union(joined), MIN_ROAD_AREA)] centerlines = [piece for l in world.centerlines for piece in (_lines(l.difference(cut)) if l.intersects(cut) else [l])] + [line] freed = unary_union(removed + [cut]).difference(corridor) covered = [b[0] for b in buildings if b[0].intersects(freed)] + \ [poly for poly, kind in world.surfaces if kind == "urban" and poly.intersects(freed)] if covered: freed = freed.difference(unary_union(covered)) surfaces = world.surfaces + [(poly, "urban") for poly in _polygons(freed)] palms = [p for p in world.palms if corridor.distance(Point(p)) >= PALM_KERB] return replace(world, roads=roads, centerlines=centerlines, buildings=buildings, surfaces=surfaces, palms=palms) def check_corners(config, roads: list[tuple[Polygon, bool]], tolerance: float = 2.5) -> None: """Jede Spielecke muss am Rand einer Straße liegen, sonst hat sich die Extraktion geändert. Gemessen wird gegen den Rand, nicht gegen die Fläche: alle vier Ecken liegen heute innerhalb der Straßenfläche, ihr Flächenabstand ist 0,000 m. Eine Prüfung auf die Fläche schlägt deshalb nur an, wenn eine Straße sich zurückzieht. Verbreitert ein künftiger Extraktionslauf die Straßen nach innen, bliebe der Flächenabstand 0, und apply_park() schnitte die verbreiterte Straße still auf das veraltete Viereck zurück — genau der Fehlerfall, den diese Prüfung abfangen soll. Am Referenzstand f32ea27 liegen die Ecken 0,45 m (sw), 0,14 m (se), 1,46 m (ne) und 1,14 m (nw) vom Straßenrand entfernt. 2,5 m Toleranz lässt der weitesten Ecke damit 1,04 m Luft, bevor die Prüfung grundlos abbricht, und schlägt umgekehrt an, sobald eine Straßenkante sich um mehr als ihren heutigen Abstand plus rund einen Meter verschiebt. """ for name, (x, y) in config.game.items(): corner = Point(x, y) if not any(poly.boundary.distance(corner) <= tolerance for poly, bridge in roads if not bridge): raise ParkError(f"Spielecke {name} ({x:g} | {y:g}) liegt weiter als {tolerance:g} m vom Rand jeder " "Straße: Ecken in flamingo-park.toml neu messen") def apply_park(world: World, layout: ParkLayout, seed: int) -> World: """Alte Straßen, Beläge, Gebäude und Palmen im Park weg, neue Anlage hinein, Palmen neu gesetzt.""" quad = layout.lawn roads = [(piece, bridge) for poly, bridge in world.roads for piece in (_polygons(poly.difference(quad), MIN_ROAD_AREA) if poly.intersects(quad) else [poly])] centerlines = [piece for line in world.centerlines for piece in (_lines(line.difference(quad)) if line.intersects(quad) else [line])] surfaces = [(piece, kind) for poly, kind in world.surfaces for piece in (_polygons(poly.difference(quad)) if poly.intersects(quad) else [poly])] buildings = [b for b in world.buildings if not b[0].intersects(quad) or b[0].intersection(quad).area <= 0.5 * b[0].area] palms = [p for p in world.palms if not quad.contains(Point(p))] roads += [(poly, False) for poly in layout.service] surfaces += [(quad, "park")] + layout.surfaces buildings += [(poly, {"height": height, "source": "osm"}, False) for poly, height in layout.buildings] reach = quad.buffer(PALM_CLEARANCE + 1) blocked = unary_union([poly for poly, _ in roads if poly.intersects(reach)] + [b[0] for b in buildings if b[0].intersects(reach)] + layout.paths + layout.keepouts).intersection(reach) lawn = quad.difference(unary_union([poly for poly, _ in layout.surfaces])) # Abstand zu einer leeren Geometrie ist in shapely NaN, und NaN > 6 verwürfe jede Palme. palms += palm_points([], _polygons(lawn), [], [] if blocked.is_empty else [blocked], [], [], seed) return replace(world, roads=roads, centerlines=centerlines, surfaces=surfaces, buildings=buildings, palms=palms, paths=world.paths + layout.paths) def apply_flamingo_park(world: World, config_path: Path, seed: int) -> tuple[World, list[str]]: """Alles für Flamingo Park in der Reihenfolge der Spec: Ecken prüfen, Jefferson Avenue, Park.""" config = load_park(config_path) check_corners(config, world.roads) world = apply_street(world, config.street) fit = ParkFit(config) layout = build_layout(load_elements(config.osm_path), config, fit) world = apply_park(world, layout, seed) messages = [f"Warnung: {warning}" for warning in layout.warnings] messages.append(f"Flamingo Park: {len(layout.paths)} Wegflächen, {len(layout.surfaces)} Flächen, " f"{len(layout.buildings)} Gebäude, Jefferson Avenue {config.street.width:g} m breit") return world, messages