Streetmap Fetch — Programmatic OS-style Map Retrieval

Getting a static OS Explorer (1:25,000) / OS Landranger (1:50,000) style map image out of streetmap.co.uk programmatically, so John can run a script on his phone (Python Codepad app), get back a base64-encoded image, paste it into a Claude chat, and have Claude decode/view it — working around the fact that Claude's own web_fetch tool cannot reach streetmap.co.uk reliably (see Background below).

Why this exists

Claude's web_fetch tool served a stale cached response for streetmap.co.uk/map?... URLs regardless of the query parameters passed — every fetch after the first returned the same BS36 1LU content. Fetching the newer streetmap.co.uk/grid/... path format failed outright with a "URL not in any prior search or fetch result" permission error. So the plan is: John's phone does the real HTTP fetching (no such restriction there), and ships the result back as text (base64) for Claude to decode locally and view as an image.

streetmap.co.uk link format (confirmed working)

Documented at their own How to link to us page:

https://streetmap.co.uk/grid/<easting>_<northing>_<zoom>

zoom codes:
  106  Full street scale
  110  Street scale
  115  Town 25k scale   <- OS Explorer-equivalent
  120  Town 50k scale   <- OS Landranger-equivalent
  126  Full road scale
  130  Road scale
  140  County scale
  150  Region scale

Confirmed working by eye (screenshot from John's phone, 2026-08-02): https://streetmap.co.uk/grid/537667_180071_120 rendered a correct 1:50,000-style map centred on Work (Canary Wharf/Isle of Dogs), with the location marker in the right place.

Grid references (OSGB36 National Grid, EPSG:27700) were computed from the notes-store gazetteer's WGS84 lat/lon via pyproj (Transformer.from_crs('EPSG:4326', 'EPSG:27700')), cross-checked against the known BS36 1LU grid ref (365032, 180850) to within a few metres:

PlaceLat/Lon (gazetteer)OSGB36 grid ref
Home51.525196, -2.505474365029, 180819
Work51.502813, -0.017929537667, 180071

Getting the actual image: unresolved

The page's og:image meta tag looks like the obvious source for a static raster of the map (e.g. https://www.streetmap.co.uk/emm/TQ3766780071_z_25k_76.gif for Work at 25k) but fetching it directly returns a genuine 404 from streetmap's own server (Kestrel/ASP.NET Core backend, their own styled 404 HTML page) — not an anti-hotlinking block. So the filename in og:image is stale or templated rather than a real generated file. Setting Referer to the page URL didn't help, which supports "file genuinely doesn't exist" over "blocked".

Current approach (untested as of this note): instead of trusting og:image alone, scan the whole page for every .gif/.png/.jpg URL referenced anywhere (meta tags, <img>, inline JS, data- attributes) and try each in turn, keeping whichever one actually returns image bytes rather than another HTML page. Also saves the full page HTML to page.html for manual inspection if none of the candidates work.

Likely next step if that still fails: the on-screen map John showed in his screenshot is presumably rendered by JS calling a tile or print/export endpoint that isn't a simple <img src> on the static page — would need to inspect page.html's <script> blocks for the actual endpoint pattern (something parameterised by grid ref/zoom), or look for a "print this map" feature (there's a visible print icon in the UI) which may hit a dedicated static-image-generating endpoint.

Script

Current version (iterating — update this block when the script changes materially rather than only noting it verbally). Runs on John's phone via Python Codepad (needs requests); not run in Claude's own sandbox since streetmap.co.uk is not on Claude's bash-tool network allowlist.

#!/usr/bin/env python3
"""
Fetch a streetmap.co.uk map page, pull out its map image, then
gzip + base64 encode the image bytes so the result can be pasted
as plain text into a chat message.

Usage:
    python3 fetch_streetmap.py
    python3 fetch_streetmap.py "https://streetmap.co.uk/grid/537667_180071_115"

Prints the base64 text to stdout, and also writes it to
streetmap_b64.txt in the current directory (handy if it's too long
to comfortably copy from the terminal).
"""

import sys
import gzip
import base64
import re
import requests

DEFAULT_URL = "https://streetmap.co.uk/grid/537667_180071_115"  # Work, 25k


def make_session() -> requests.Session:
    ua = (
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/124.0.0.0 Safari/537.36"
    )
    sess = requests.Session()
    sess.headers.update({"User-Agent": ua})
    return sess


def find_candidate_image_urls(html: str, page_url: str) -> list:
    """Every .gif/.png/.jpg URL mentioned anywhere on the page —
    in meta tags, <img> tags, inline JS, data- attributes, etc.
    The og:image one turned out to be a stale/wrong filename, so
    cast a wide net instead of trusting a single tag."""
    from urllib.parse import urljoin

    found = re.findall(r'["\'](https?://[^"\']+?\.(?:gif|png|jpg|jpeg))["\']', html, re.IGNORECASE)
    found += re.findall(r'["\'](/[^"\']+?\.(?:gif|png|jpg|jpeg))["\']', html, re.IGNORECASE)

    seen = set()
    urls = []
    for u in found:
        full = urljoin(page_url, u)
        if full not in seen:
            seen.add(full)
            urls.append(full)
    return urls


def fetch_map_image_bytes(page_url: str) -> bytes:
    sess = make_session()

    page = sess.get(page_url, timeout=20)
    page.raise_for_status()

    with open("page.html", "w") as f:
        f.write(page.text)
    print(f"Saved full page HTML to page.html ({len(page.text)} chars)", file=sys.stderr)

    candidates = find_candidate_image_urls(page.text, page_url)
    print(f"Found {len(candidates)} candidate image URL(s):", file=sys.stderr)
    for u in candidates:
        print(f"  {u}", file=sys.stderr)

    if not candidates:
        raise RuntimeError("No image URLs found on the page — check page.html")

    for image_url in candidates:
        img = sess.get(image_url, headers={"Referer": page_url}, timeout=20)
        if img.ok and img.content[:3] not in (b"<!D", b"<ht"):
            print(f"Using: {image_url} ({len(img.content)} bytes)", file=sys.stderr)
            return img.content
        print(f"  skip {image_url}: {img.status_code}", file=sys.stderr)

    raise RuntimeError("None of the candidate image URLs loaded — check page.html by hand")


def main():
    page_url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URL

    raw_bytes = fetch_map_image_bytes(page_url)
    print(f"Fetched image: {len(raw_bytes)} bytes", file=sys.stderr)

    gzipped = gzip.compress(raw_bytes)
    print(f"Gzipped: {len(gzipped)} bytes", file=sys.stderr)

    b64 = base64.b64encode(gzipped).decode("ascii")
    print(f"Base64: {len(b64)} chars", file=sys.stderr)

    with open("streetmap_b64.txt", "w") as f:
        f.write(b64)
    print("Wrote streetmap_b64.txt", file=sys.stderr)

    print(b64)


if __name__ == "__main__":
    main()

Status / next steps

• Not yet resolved: run the current "scan every image URL" version and report back which (if any) candidate URL actually loads a real image.

• If none load, inspect page.html's <script> blocks for the real tile/print endpoint the interactive map uses.

• Once an image is successfully retrieved end-to-end (base64 pasted into chat, Claude decodes/gunzips/views it), record the working approach here and simplify the script back down.

Related notes

gazetteer — source of Home/Work coordinates used here

using-gps-data — broader GPS/location conventions

created 2026-08-02  ·  tags gps, location, python, in-progress  ·  updated 2026-08-02  ·  version 1