Skip to content

API reference

Auto-generated from docstrings. These are the stable, load-bearing public functions; internal helpers are omitted on purpose.

The HARD-RULE filter

tracelines.models.is_official_panoid(pid)

Positive whitelist for official Google pano ids (Layer-1 HARD-RULE guard).

Stricter and safer than streetlevel's prefix denylist: it positively confirms the 22-char / 16-byte / terminal-{A,Q,g,w} invariant, which rejects the documented false-negative classes (legacy 22-char photospheres, the new CIAB/AF1Q UGC schemes) that a CIHM0og/len>22 denylist would let through. Verified 2026-07-13 against 628 live official Bucharest ids (100% accept) and live CIHM photosphere ids (100% reject).

Source code in tracelines/models.py
def is_official_panoid(pid: str) -> bool:
    """Positive whitelist for official Google pano ids (Layer-1 HARD-RULE guard).

    Stricter and safer than streetlevel's prefix denylist: it *positively*
    confirms the 22-char / 16-byte / terminal-{A,Q,g,w} invariant, which rejects
    the documented false-negative classes (legacy 22-char photospheres, the new
    CIAB/AF1Q UGC schemes) that a `CIHM0og`/`len>22` denylist would let through.
    Verified 2026-07-13 against 628 live official Bucharest ids (100% accept) and
    live CIHM photosphere ids (100% reject).
    """
    if not isinstance(pid, str) or len(pid) != 22:
        return False
    if pid[-1] not in _OFFICIAL_TERMINAL:
        return False
    if pid.startswith(_THIRD_PARTY_PREFIXES):
        return False
    try:
        raw = base64.urlsafe_b64decode(pid + "==")
    except Exception:
        return False
    return len(raw) == 16

Data model

tracelines.models.Pano dataclass

A single coverage node (one panorama / image point), provider-agnostic.

Google/KartaView emit these and rely on graph stitching; Mapillary emits native segments directly (see CoverageSegment).

Source code in tracelines/models.py
@dataclass
class Pano:
    """A single coverage node (one panorama / image point), provider-agnostic.

    Google/KartaView emit these and rely on graph stitching; Mapillary emits
    native segments directly (see CoverageSegment).
    """

    id: str
    lon: float
    lat: float
    source: Source
    heading: float | None = None
    is_third_party: bool = False  # True => user photosphere ("circle")
    sv_source: str | None = None  # google: 'launch'(car)/'scout'(trekker)/'innerspace'...
    date: str | None = None  # 'YYYY-MM' or ISO
    neighbors: list[str] = field(default_factory=list)  # adjacent pano ids => graph edges
    is_pano: bool | None = None  # mapillary: 360 flag
    raw: Any = None

    @property
    def degree(self) -> int:
        return len(self.neighbors)

tracelines.models.CoverageSegment dataclass

A continuous coverage polyline — one of the "blue lines".

Source code in tracelines/models.py
@dataclass
class CoverageSegment:
    """A continuous coverage polyline — one of the "blue lines"."""

    coords: list[tuple[float, float]]  # [(lon,lat), ...] in EPSG:4326
    source: Source
    id: str | None = None
    pano_ids: list[str] = field(default_factory=list)
    capture_date: str | None = None
    is_pano: bool | None = None
    meta: dict = field(default_factory=dict)

    @property
    def length_m(self) -> float:
        return line_length_m(self.coords)

    @property
    def n_points(self) -> int:
        return len(self.coords)

    def to_geojson_feature(self) -> dict:
        src = self.source.value if isinstance(self.source, Source) else self.source
        props: dict[str, Any] = {
            "source": src,
            "length_m": round(self.length_m, 2),
            "n_points": self.n_points,
        }
        if self.id:
            props["id"] = self.id
        if self.capture_date:
            props["capture_date"] = self.capture_date
        if self.is_pano is not None:
            props["is_pano"] = self.is_pano
        if self.pano_ids:
            props["pano_ids"] = self.pano_ids
        props.update(self.meta)
        return {
            "type": "Feature",
            "geometry": {
                "type": "LineString",
                "coordinates": [[round(x, 7), round(y, 7)] for x, y in self.coords],
            },
            "properties": props,
        }

length_m property

n_points property

to_geojson_feature()

Source code in tracelines/models.py
def to_geojson_feature(self) -> dict:
    src = self.source.value if isinstance(self.source, Source) else self.source
    props: dict[str, Any] = {
        "source": src,
        "length_m": round(self.length_m, 2),
        "n_points": self.n_points,
    }
    if self.id:
        props["id"] = self.id
    if self.capture_date:
        props["capture_date"] = self.capture_date
    if self.is_pano is not None:
        props["is_pano"] = self.is_pano
    if self.pano_ids:
        props["pano_ids"] = self.pano_ids
    props.update(self.meta)
    return {
        "type": "Feature",
        "geometry": {
            "type": "LineString",
            "coordinates": [[round(x, 7), round(y, 7)] for x, y in self.coords],
        },
        "properties": props,
    }

tracelines.models.Source

Bases: str, Enum

Source code in tracelines/models.py
class Source(str, Enum):
    GOOGLE = "google"
    MAPILLARY = "mapillary"
    KARTAVIEW = "kartaview"

Extraction

tracelines.pipeline.extract(bbox, source_names, settings)

Source code in tracelines/pipeline.py
def extract(bbox, source_names, settings):
    return asyncio.run(extract_async(bbox, source_names, settings))

tracelines.nearest.find_nearest_coverage(lat, lon, *, radius_tiles=1, require_connected=True, verify_source=True, include_trekker=False)

Live: nearest official continuous-coverage pano to (lat, lon).

Never returns a third-party pano. Searches the z17 tile containing the point plus a (2*radius_tiles+1)^2 neighborhood so border cases are covered.

Source code in tracelines/nearest.py
def find_nearest_coverage(
    lat: float,
    lon: float,
    *,
    radius_tiles: int = 1,
    require_connected: bool = True,
    verify_source: bool = True,
    include_trekker: bool = False,
) -> Nearest | None:
    """Live: nearest official continuous-coverage pano to (lat, lon).

    Never returns a third-party pano. Searches the z17 tile containing the point
    plus a (2*radius_tiles+1)^2 neighborhood so border cases are covered.
    """
    if not _AVAILABLE:
        raise RuntimeError(f"streetlevel not importable: {_IMPORT_ERR}")

    tx, ty = _sv.wgs84_to_tile_coord(lat, lon, Z)
    seen: dict[str, Pano] = {}
    for dx in range(-radius_tiles, radius_tiles + 1):
        for dy in range(-radius_tiles, radius_tiles + 1):
            for p in _sv.get_coverage_tile(tx + dx, ty + dy):
                if p.id not in seen:
                    seen[p.id] = _pano_from_sl(p)

    ranked = nearest_official(list(seen.values()), lon, lat, require_connected)
    if not ranked:
        return None

    keep = KEEP_SOURCES_TREKKER if include_trekker else KEEP_SOURCES_CAR
    for cand in ranked:
        # HARD RULE belt-and-suspenders: never a third-party pano
        if cand.is_third_party:
            continue
        if verify_source:
            q = _sv.find_panorama_by_id(cand.id)
            src = getattr(q, "source", None) if q else None
            cand.sv_source = src
            cand.date = _fmt_date(getattr(q, "date", None)) if q else None
            if src is not None and src not in keep:
                continue  # trekker/indoor/etc. — want car lines
        return Nearest(pano=cand, distance_m=haversine_m(lon, lat, cand.lon, cand.lat))

    # Every candidate failed the (optional) source filter but all were official +
    # connected — return the nearest official connected node (still never third-party).
    best = ranked[0]
    return Nearest(pano=best, distance_m=haversine_m(lon, lat, best.lon, best.lat))

tracelines.nearest.nearest_official(cands, lon, lat, require_connected=True)

Pure: apply the HARD RULE and return official candidates, nearest first.

Drops every third-party pano (photosphere/photopet) via BOTH the streetlevel denylist (is_third_party) AND the positive is_official_panoid whitelist. With require_connected, also drops isolated official dots (degree 0).

Source code in tracelines/nearest.py
def nearest_official(
    cands: list[Pano], lon: float, lat: float, require_connected: bool = True
) -> list[Pano]:
    """Pure: apply the HARD RULE and return official candidates, nearest first.

    Drops every third-party pano (photosphere/photopet) via BOTH the streetlevel
    denylist (is_third_party) AND the positive is_official_panoid whitelist. With
    require_connected, also drops isolated official dots (degree 0).
    """
    official = [p for p in cands if not p.is_third_party and is_official_panoid(p.id)]
    if require_connected:
        official = [p for p in official if p.degree >= 1]
    official.sort(key=lambda p: haversine_m(lon, lat, p.lon, p.lat))
    return official

tracelines.probe.density_probe(bbox, source_names, settings)

Source code in tracelines/probe.py
def density_probe(bbox, source_names, settings):
    return asyncio.run(density_probe_async(bbox, source_names, settings))

Export

tracelines.export.write_geojson(segments, path)

Source code in tracelines/export.py
def write_geojson(segments: Iterable[CoverageSegment], path: str) -> dict:
    fc = to_feature_collection(segments)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(fc, f, ensure_ascii=False)
    return fc

tracelines.export.summary_stats(segments)

Source code in tracelines/export.py
def summary_stats(segments: Iterable[CoverageSegment]) -> dict:
    segments = list(segments)
    by_src: Counter[str] = Counter()
    km_by_src: dict[str, float] = {}
    years: list[str] = []
    for s in segments:
        src = s.source.value if isinstance(s.source, Source) else str(s.source)
        by_src[src] += 1
        km_by_src[src] = km_by_src.get(src, 0.0) + s.length_m / 1000.0
        if s.capture_date:
            years.append(str(s.capture_date)[:4])
    total_km = sum(km_by_src.values())
    median_year = None
    if years:
        ys = sorted(years)
        median_year = ys[len(ys) // 2]
    return {
        "segments": len(segments),
        "total_km": round(total_km, 3),
        "median_capture_year": median_year,
        "by_source": {
            k: {"segments": by_src[k], "km": round(km_by_src[k], 3)} for k in sorted(by_src)
        },
    }

Configuration

tracelines.config.Settings dataclass

Source code in tracelines/config.py
@dataclass
class Settings:
    # concurrency / networking
    concurrency: int = 16
    timeout: float = 30.0
    max_retries: int = 4
    retry_base: float = 0.5  # exponential backoff base seconds
    request_jitter: float = 0.15  # +/- jitter fraction on backoff

    # tiling
    zoom_google: int = 17  # streetlevel coverage tiles
    zoom_mapillary: int = 14  # mapillary sequence layer max zoom

    # filtering ("circle" exclusion)
    min_run: int = 2  # min points/panos for a kept segment
    # DEBUG/RESEARCH ONLY. Setting this True disables BOTH Layer-1 guards and lets
    # Google-hosted photospheres/photo-paths into the output — it VIOLATES the blue-line
    # HARD RULE. Not exposed on the CLI. Never enable for the coverage deliverable.
    include_third_party: bool = False
    include_trekker: bool = False  # google: keep source in {launch, scout} vs launch-only
    precision: bool = (
        False  # google: find_panorama_by_id per pano (adds source/date, cross-tile links)
    )

    # fusion
    snap: bool = False  # snap onto OSM network (needs osmnx)
    dedupe: bool = False  # cross-source spatial dedup (default: union w/ provenance)
    dedupe_buffer_m: float = 12.0  # dedup buffer radius, meters
    dedupe_min_overlap: float = 0.6  # drop lower-priority seg if >= this fraction lies in buffer
    source_priority: tuple = ("google", "mapillary", "kartaview")

    # storage
    cache_dir: str = ".svcache"
    cache_enabled: bool = True

    # identity / secrets (from env; never hardcode)
    user_agent: str = "tracelines/0.1 (research; contact owner)"
    mapillary_token: str | None = field(
        default_factory=lambda: _env("MAPILLARY_TOKEN", "MAPILLARY_ACCESS_TOKEN")
    )
    google_api_key: str | None = field(
        default_factory=lambda: _env("MAPS_API_TOKEN", "STREETVIEW_API_KEY", "GOOGLE_MAPS_API_KEY")
    )

    def resolve_area(self, area: str | None, bbox: BBox | None) -> BBox:
        if bbox is not None:
            return bbox
        if area is not None:
            key = area.strip().lower()
            if key in AREAS:
                return AREAS[key]
            raise ValueError(f"unknown area '{area}'. known: {sorted(AREAS)}")
        raise ValueError("must pass area or bbox")