Old Map Projections Explained: What Grid Was a Historical Map Drawn On?

On this page

Problem statement

Every flat map is drawn on some projection, and an old map's projection decides how it should be georeferenced. If it was drawn on a known projection with known sheet lines, its corners can be computed and its geometry restored exactly; if it was compiled by eye from travellers' distances, there is no projection to recover and the best you can do is a fitted transformation. Treating the first kind like the second throws information away; treating the second like the first invents precision.

This page tests both kinds on real maps of Rutland. The Ordnance Survey six-inch sheets of 1884–1891 belong to the County Series, drawn on a Cassini projection centred on a meridian through Dunnose on the Isle of Wight; John Speed's county map of 1611 was not drawn on any projection in the modern sense. The georeferenced sheets themselves show which is which.

Quick answer

Put the georeferenced sheet corners into the projection the series is supposed to use and check whether they fall on the sheet lines. For the County Series that is Cassini on the Dunnose meridian, with quarter sheets 3 miles wide:

for name in SHEETS:
    e, n = corner_bng(name)                                   # top-left corner from the georeferencing
    x_ft, y_ft = [v / FOOT for v in to_cassini.transform(e, n)]
    print(f"{name}: {x_ft:9,.0f} ft east of the Dunnose meridian = {x_ft / 15840:6.3f} x 3 miles")
009-NW:    95,051 ft east of the Dunnose meridian =  6.001 x 3 miles
009-NE:   110,882 ft east of the Dunnose meridian =  7.000 x 3 miles
010-NW:   126,713 ft east of the Dunnose meridian =  8.000 x 3 miles

The western edges of three adjacent sheets sit within a few feet of whole multiples of three miles from the meridian — six, seven and eight sheet-widths east of it — so the County Series geometry is recovered from nothing more than seven control points on one sheet:

The three 1880s six-inch sheets 009-NW, 009-NE and 010-NW side by side in British National Grid, with dashed red lines at 18, 21, 24 and 27 miles east of the Dunnose meridian in the Cassini projection; the lines run exactly along the sheet edges and joins. The three 1880s six-inch sheets 009-NW, 009-NE and 010-NW side by side in British National Grid, with dashed red lines at 18, 21, 24 and 27 miles east of the Dunnose meridian in the Cassini projection; the lines run exactly along the sheet edges and joins.
The County Series sheet lines, computed from the projection alone, against sheets placed by seven control points. OS six-inch sheets, public domain.

Find the series' projection, check sheet lines

Testing whether a georeferenced old map sheet sits on its series’ projection. does the sheet sit on its projection?1look up the seriesCounty Series, Dunnose2corners into Cassinifrom the georeferencing3compare with sheet lines3 miles: within 11 ft4rotation against BNG0.64° predicted, 0.48° fitted5no projection? test errorsSpeed: local, irregularA passing test means the sheet can be placed by arithmetic spatialworkflow.io
How to find out what grid an old map was drawn on.

1. Find out what the series used

The projection is a historical fact, not something to guess. For the County Series the National Library of Scotland's notes on the six-inch maps record that Rutland was one of the central English counties drawn on the Dunnose meridian, a Cassini projection whose central meridian passes through Dunnose on the Isle of Wight, 1°11′50.136″ W, 50°37′03.748″ N:

op = DUNNOSE.source_crs.coordinate_operation
print(op.method_name, "on", DUNNOSE.ellipsoid.name)
print({p.name: round(p.value, 6) for p in op.params})
Cassini-Soldner on Airy 1830
{'Latitude of natural origin': 50.617708, 'Longitude of natural origin': -1.19726, 'False easting': 0.0, 'False northing': 0.0}

The ellipsoid is Airy 1830, the one the National Grid still uses; the datum shift to OSGB 1936 here uses its usual seven parameters, which is close enough for a test at the scale of feet.

2. Check the sheet lines

Quarter sheets of the six-inch County Series are 3 miles east–west and 2 miles north–south. If the georeferenced corners fall on multiples of those distances from the meridian, the georeferencing and the projection agree:

for name in SHEETS:
    e, n = corner_bng(name)
    x_ft, y_ft = [v / FOOT for v in to_cassini.transform(e, n)]
    print(f"{name}: east {x_ft % 15840:7.0f} ft past a 3-mile line;  north {y_ft % 10560:7.0f} ft past a 2-mile line")
009-NW: east      11 ft past a 3-mile line;  north    1405 ft past a 2-mile line
009-NE: east       2 ft past a 3-mile line;  north    1361 ft past a 2-mile line
010-NW: east   15833 ft past a 3-mile line;  north    1317 ft past a 2-mile line

East–west, within a few feet. North–south the corners do not fall on 2-mile multiples counted from Dunnose's latitude; the rows of County Series sheets were evidently laid out from a different starting line, which this test cannot identify. The east–west agreement is the striking result: it is what a Cassini grid predicts and an arbitrary fit does not.

3. See what the projection does to the National Grid

The Cassini grid and the National Grid are both transverse projections of the same ellipsoid, on different central meridians: Dunnose, 1.2° W, and the National Grid's 2° W. At Oakham their north directions differ:

e, n = 486065.6, 308906.1
x, y = to_cassini.transform(e, n)
a, b = np.array(from_cassini.transform(x, y)), np.array(from_cassini.transform(x, y + 1000))
print(f"County Series north points {np.degrees(np.arctan2(*(b - a))):+.2f} deg from National Grid north at Oakham; "
      f"1 km of Cassini = {np.hypot(*(b - a)):.2f} m of National Grid")
County Series north points -0.64 deg from National Grid north at Oakham; 1 km of Cassini = 999.66 m of National Grid

That is the rotation every georeferenced six-inch sheet of Rutland shows. The affine fit to seven control points measured about half a degree; the projection says 0.64°. The difference, 0.16° across a 4.8 km sheet, is about 13 m at its edge — inside the control points' error, and the reason the corner tests in step 2 drift by a few dozen feet from sheet to sheet.

4. Test a map that had no projection

Speed's map has no sheet lines to test, so test its distortion instead. A map drawn on a projection has smooth, predictable distortion; a compiled map has local, irregular error. The leave-one-out errors of the early county map guide show which:

for order in (1, 2):
    X = design(SPEED, order)
    W = np.array([[g[3], g[4]] for g in SPEED])
    lo = [np.hypot(*(X[i] @ np.linalg.lstsq(np.delete(X, i, 0), np.delete(W, i, 0), rcond=None)[0] - W[i]))
          for i in range(len(SPEED))]
    print(f"Speed, polynomial order {order}: leave-one-out RMS {np.sqrt(np.mean(np.square(lo))):5.0f} m")
Speed, polynomial order 1: leave-one-out RMS   695 m
Speed, polynomial order 2: leave-one-out RMS   549 m
Leave-one-out RMS error of Speed's 1611 map of Rutland with a first- and a second-order polynomial, in metres. Speed's map: each village predicted from the restaffine (order 1)695 mpolynomial order 2549 m0400 m800 msmooth bending removes 21%What a smooth transformation cannot remove is local error spatialworkflow.io
Bending the whole map smoothly removes a fifth of the error; the rest is local. The cell-by-cell picture is in the distortion grid guide.

A smooth transformation removes about a fifth of the error; the rest is village by village. That is the signature of a compiled map: no projection, or none that survives the compilation, to recover.

Sheet corners from the Cassini projection alone

The data used on this page

Ordnance Survey six-inch sheets Rutland 009-NW, 009-NE and 010-NW (1884–1891) and John Speed's Rutlandshire (1676 edition of the 1611 map, David Rumsey Map Collection, Stanford Libraries), all from Wikimedia Commons (public domain); OpenStreetMap from Geofabrik (1 January 2026; © OpenStreetMap contributors, ODbL). The Dunnose origin is from the National Library of Scotland's notes on the six-inch County Series.

from pathlib import Path

import geopandas as gpd
import numpy as np
import requests
from PIL import Image
from pyproj import CRS, Proj, Transformer

Path("data").mkdir(exist_ok=True)
UA = {"User-Agent": "spatialworkflow.io guide (https://www.spatialworkflow.io)"}
COMMONS = "https://upload.wikimedia.org/wikipedia/commons/"
for name, path in {"009-NW": "a/a0/", "009-NE": "8/80/", "010-NW": "4/47/"}.items():
    if not Path(f"data/os_{name}.jpg").exists():
        url = f"{COMMONS}{path}Map_of_Rutland_OS_Map_name_{name}%2C_Ordnance_Survey%2C_1884-1891.jpg"
        Path(f"data/os_{name}.jpg").write_bytes(requests.get(url, headers=UA, timeout=300).content)
if not Path("data/rutland.shp.zip").exists():
    Path("data/rutland.shp.zip").write_bytes(requests.get(
        "https://download.geofabrik.de/europe/united-kingdom/england/rutland-260101-free.shp.zip",
        headers=UA, timeout=300).content)

GCPS = [  # (id, pixel, line, easting, northing) on 009-NW
    ("oakham_church",          3141,  487, 486065.6, 308906.1),
    ("egleton_church",         4824, 2031, 487598.5, 307540.3),
    ("brooke_rd_crossing",     2956, 1159, 485892.0, 308306.8),
    ("egleton_level_crossing", 3998, 2481, 486838.1, 307119.0),
    ("stamford_rd_guide_post", 5127,  988, 487872.3, 308486.1),
    ("braunston_rd_bridleway", 1263, 1675, 484382.4, 307839.2),
    ("brooke_guide_post",      1547, 3292, 484648.2, 306376.1),
]
SHEETS = {"009-NW": 0, "009-NE": 1, "010-NW": 2}            # sheet widths east of 009-NW
DUNNOSE = CRS.from_proj4("+proj=cass +lat_0=50.617708 +lon_0=-1.197260 +x_0=0 +y_0=0 +ellps=airy "
                         "+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs")
to_cassini = Transformer.from_crs(27700, DUNNOSE, always_xy=True)
from_cassini = Transformer.from_crs(DUNNOSE, 27700, always_xy=True)
FOOT = 0.3048
to_ll = Transformer.from_crs(27700, 4326, always_xy=True)

X = np.array([[1, g[1], g[2]] for g in GCPS], float)
A = np.linalg.lstsq(X, np.array([[g[3], g[4]] for g in GCPS]), rcond=None)[0]
W0 = Image.open("data/os_009-NW.jpg").size[0]

def corner_bng(name):
    """Top-left corner of a sheet in British National Grid, from 009-NW's fit and the sheet layout."""
    return tuple(np.array([1, SHEETS[name] * W0, 0]) @ A)

SPEED_PX = {  # modern village name: (column, row) of Speed's church symbol
    "Oakham": (4960, 4035), "Langham": (4475, 3725), "Barleythorpe": (4755, 3920), "Egleton": (5265, 4290),
    "Braunston in Rutland": (4470, 4745), "Brooke": (4705, 4790), "Gunthorpe": (5095, 4745), "Manton": (5430, 4935),
    "Greetham": (6200, 2930), "Cottesmore": (5715, 3205), "Exton": (6165, 3500), "Empingham": (6730, 3890),
    "Whitwell": (6245, 4080), "Upper Hambleton": (5795, 4285), "Burley on the Hill": (5490, 3705), "Barrow": (5495, 2965),
    "Pickworth": (7375, 2930), "Tickencote": (7595, 3875), "Lyndon": (6005, 4880), "Edith Weston": (6415, 4555),
    "Wing": (5735, 5245), "Pilton": (6315, 5335), "North Luffenham": (6600, 5110), "South Luffenham": (6705, 5400),
    "Ridlington": (4755, 5425), "Preston": (5335, 5380), "Glaston": (5650, 5690), "Morcott": (6465, 5735),
    "Ayston": (4960, 5765), "Uppingham": (5125, 6080), "Bisbrooke": (5605, 6035), "Seaton": (5960, 6430),
    "Barrowden": (7070, 5985),
}
places = gpd.read_file("/vsizip/data/rutland.shp.zip/gis_osm_places_free_1.shp").to_crs(27700)
places = places[places.fclass.isin(["village", "town", "hamlet"])].drop_duplicates("name").set_index("name")
SPEED = [(k, c, r, places.loc[k].geometry.x, places.loc[k].geometry.y) for k, (c, r) in SPEED_PX.items()]

def design(points, order):
    c = np.array([g[1] for g in points], float) / 1e4
    r = np.array([g[2] for g in points], float) / 1e4
    cols = [np.ones_like(c), c, r] + ([c * c, c * r, r * r] if order == 2 else [])
    return np.column_stack(cols)

print([round(v) for v in corner_bng("009-NW")])
[483228, 309330]

Example 1: compute a sheet's corners from the projection alone

If the County Series grid is right, a sheet's corners can be computed without any control points at all — from the meridian, the sheet size, and which sheet it is. Taking the nearest 3-mile line to 009-NW's western edge, and the northing the georeferencing gave:

e, n = corner_bng("009-NW")
x, y = to_cassini.transform(e, n)
x_line = round(x / FOOT / 15840) * 15840 * FOOT                        # the 3-mile sheet line
predicted = [from_cassini.transform(x_line + dx * 15840 * FOOT, y - dy * 10560 * FOOT) for dx, dy in ((0, 0), (1, 0), (1, 1), (0, 1))]
fitted = [tuple(np.array([1, c, r]) @ A) for c, r in ((0, 0), (5343, 0), (5343, 3573), (0, 3573))]
for (pe, pn), (fe, fn) in zip(predicted, fitted):
    print(f"projection {pe:10.1f} {pn:10.1f}   control points {fe:10.1f} {fn:10.1f}   apart {np.hypot(pe - fe, pn - fn):5.1f} m")
projection   483224.9   309329.6   control points   483228.1   309329.6   apart   3.2 m
projection   488051.1   309383.4   control points   488051.8   309370.1   apart  13.4 m
projection   488086.9   306166.0   control points   488071.2   306152.9   apart  20.5 m
projection   483260.8   306112.2   control points   483247.5   306112.4   apart  13.3 m

Corners from the projection and corners from seven hand-picked control points agree to within the control points' own error. On a sheet with no surviving features to use as control points, the projection alone would place it.

Example 2: the scale of the Cassini grid

Cassini keeps scale true along the central meridian; away from it, distances north–south on the map are stretched, increasingly with distance east or west. pyproj reports the projection's own scale factors at any point:

proj = Proj(DUNNOSE.source_crs)
for name in SHEETS:
    lon, lat = to_ll.transform(*corner_bng(name))
    f = proj.get_factors(lon, lat)
    x, _ = to_cassini.transform(*corner_bng(name))
    print(f"{name}: {x / 1000:5.1f} km east of the meridian; north-south scale {f.meridional_scale:.7f} "
          f"= {(f.meridional_scale - 1) * 1e6:4.1f} mm per km")
009-NW:  29.0 km east of the meridian; north-south scale 1.0000102 = 10.2 mm per km
009-NE:  33.8 km east of the meridian; north-south scale 1.0000139 = 13.9 mm per km
010-NW:  38.6 km east of the meridian; north-south scale 1.0000182 = 18.2 mm per km

A centimetre or two per kilometre — invisible at six inches to the mile. Counties far from their meridian had larger errors, because the stretch grows with the square of the distance; that is one reason the National Grid replaced the County Series' many origins with one.

A projection is a rule

A projection is the rule that turns positions on the curved Earth into positions on flat paper. Any systematically surveyed map has one, chosen by the surveyors and applied consistently; maps compiled from earlier maps and descriptions inherit whatever their sources had, blurred by the compilation, and often end up with no consistent projection at all.

A surveyed map on a known projection compared with a compiled map without one. surveyed on a projection, or compiled?OS County Series, 1880s✓Cassini on the Dunnose meridian✓sheet edges on 3-mile lines✓corners computable: 3–20 m✓distortion smooth, tinySpeed, 1611✓compiled from maps and travel✓no sheet lines to test✓smooth fit removes a fifth✓error village by villageThe first is restored; the second can only be fitted spatialworkflow.io
A projection can be restored; a compilation can only be fitted.

The Ordnance Survey's County Series used the Cassini projection, which places the paper along a central meridian and measures distances east or west of it along great circles. Each group of counties had its own meridian to keep the distortion small — Rutland's is Dunnose's, shared with much of central and southern England. Sheet lines were drawn at fixed distances from that meridian, so a sheet's position is a matter of arithmetic once the meridian is known. The test in step 2 recovers that arithmetic from a georeferenced scan, to within a few feet.

The National Grid, from the 1930s, uses a Transverse Mercator projection on a single meridian at 2° W for the whole country. Converting between the two is not just a shift: the two grids' north directions differ by an amount that depends on where you are, about two-thirds of a degree at Oakham — the rotation every affine fit to a Rutland six-inch sheet picks up.

A compiled map like Speed's is different in kind. Its geometry is the sum of many local judgements, and its errors are local. There is no projection to put it on; the honest approach is the empirical one — a fitted transformation, a leave-one-out error, and a distortion grid to show where the map is good and where it is not.

County meridians, datums and old feet

The County Series Cassini grid and the National Grid compared at Oakham. two grids over RutlandCounty SeriesNational GridprojectionCassini-SoldnerTransverse Mercatorcentral meridianDunnose, 1.20° W2° WellipsoidAiry 1830Airy 1830north at Oakham0.64° from BNG northgrid northused forCounty Series sheetsOS maps from 1938Same ellipsoid, different meridians: hence the rotation spatialworkflow.io
Same ellipsoid, different meridians.
  • Look up the projection; county origins and sheet systems are documented for the County Series.
  • Several counties shared a meridian, so sheets join across those county boundaries but not across others.
  • North–south sheet lines did not test cleanly here; a test that fails is information, not something to fit away.
  • Datum shifts at the scale of feet need care; the seven-parameter transformation used here is good to a few metres.
  • Old foot definitions differ by parts per million; irrelevant at six inches to the mile.
  • No projection is a valid answer for compiled maps; do not invent one.

FAQ

What projection did the Ordnance Survey County Series use?

The Cassini projection, on a set of county origins. Rutland's six-inch and 1:2500 sheets used the Dunnose meridian on the Isle of Wight, shared with much of central and southern England.

How can I tell whether an old map has a projection?

Check the documentation for the series first. Empirically, a surveyed map on a known projection has smooth distortion and sheet corners that fall on regular lines in that projection; a compiled map has local, irregular errors that no smooth transformation removes.

Why is my georeferenced six-inch sheet rotated against the National Grid?

The County Series grid and the National Grid have different central meridians, so their north directions differ — by about 0.64° at Oakham.

Did John Speed use a map projection?

Not in a form that can be recovered from his map of Rutland: its errors are village by village, and a smooth transformation removes only about a fifth of them.