diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts
index fbe6546..f6b1538 100644
--- a/e2e/tests/journeys.spec.ts
+++ b/e2e/tests/journeys.spec.ts
@@ -1758,3 +1758,87 @@ test('the homepage snippet names what gov.uk does not publish', async ({ page })
// Admissions distance is the one fact the DfE service has no equivalent for.
expect(desc).toMatch(/close you had to live|distance/i);
});
+
+/*
+ * The location layer (spec 2026-08-21, W2).
+ *
+ * Location intent sat at position 49.5 with one click across the whole 16-month
+ * baseline — the site published no page about a place. These assert the four
+ * families render, stay in their own namespaces, and reach the sitemap.
+ */
+async function firstPlaceOfKind(page: Page, kind: string) {
+ const res = await page.request.get('/api/places');
+ expect(res.ok()).toBeTruthy();
+ const { places } = await res.json();
+ const hit = places.find((p: { kind: string }) => p.kind === kind);
+ expect(hit, `no ${kind} in the registry`).toBeTruthy();
+ return hit as { kind: string; slug: string; name: string; count: number };
+}
+
+for (const [kind, prefix, article] of [
+ ['town', '/schools/', 'a'],
+ ['authority', '/schools/authority/', 'an'],
+ ['outcode', '/schools/near/', 'an'],
+] as const) {
+ test(`${article} ${kind} page renders with its school count`, async ({ page }) => {
+ const place = await firstPlaceOfKind(page, kind);
+ await page.goto(`${prefix}${place.slug}`);
+ await expect(page.locator('h1')).toContainText(place.name, { ignoreCase: true });
+ await expect(page.locator('a[href^="/school/"]').first()).toBeVisible();
+ });
+}
+
+test('a town and an authority sharing a name are different pages', async ({ page }) => {
+ // 67 real collisions, and the authority is the larger set in only 43 — so
+ // one namespace would have published near-duplicates.
+ const { places } = await (await page.request.get('/api/places')).json();
+ const townSlugs = new Set(
+ places.filter((p: { kind: string }) => p.kind === 'town')
+ .map((p: { slug: string }) => p.slug));
+ const clash = places.find((p: { kind: string; slug: string }) =>
+ p.kind === 'authority' && townSlugs.has(p.slug));
+ test.skip(!clash, 'no town/authority name collision in this environment');
+
+ const townRes = await page.request.get(`/api/places/town/${clash.slug}`);
+ const laRes = await page.request.get(`/api/places/authority/${clash.slug}`);
+ expect(townRes.ok() && laRes.ok()).toBeTruthy();
+ const townUrns = (await townRes.json()).schools.map((s: { urn: number }) => s.urn).sort();
+ const laUrns = (await laRes.json()).schools.map((s: { urn: number }) => s.urn).sort();
+ expect(townUrns).not.toEqual(laUrns);
+});
+
+test('a place below the threshold has no page', async ({ page }) => {
+ // Crosby holds one school; publishing it would be a page with nothing to say.
+ const res = await page.request.get('/api/places/town/crosby');
+ expect(res.status()).toBe(404);
+});
+
+test('place pages declare a canonical and reach the sitemap', async ({ page }) => {
+ const place = await firstPlaceOfKind(page, 'town');
+ await page.goto(`/schools/${place.slug}`);
+ const canonical = await page.locator('link[rel="canonical"]').first()
+ .getAttribute('href');
+ expect(canonical).toBe(`https://www.schoolcompare.co.uk/schools/${place.slug}`);
+
+ const xml = await (await page.request.get('/sitemaps/places-1.xml')).text();
+ expect(xml).toContain(`/schools/${place.slug}`);
+});
+
+test('the place page ships ItemList structured data that parses', async ({ page }) => {
+ const place = await firstPlaceOfKind(page, 'town');
+ await page.goto(`/schools/${place.slug}`);
+ const raw = await page.locator('script[type="application/ld+json"]').first()
+ .textContent();
+ const parsed = JSON.parse(raw!);
+ const types = (parsed['@graph'] ?? []).map((n: { '@type': string }) => n['@type']);
+ expect(types).toContain('ItemList');
+ expect(types).toContain('BreadcrumbList');
+});
+
+test('a place page states the local average against England', async ({ page }) => {
+ // The one number a list cannot give, and the reason these pages are not
+ // a name dropped into a template.
+ const place = await firstPlaceOfKind(page, 'town');
+ await page.goto(`/schools/${place.slug}`);
+ await expect(page.getByTestId('local-vs-england')).toContainText(/across England/i);
+});
diff --git a/nextjs-app/__tests__/components/PlaceView.test.tsx b/nextjs-app/__tests__/components/PlaceView.test.tsx
index c1685c1..9d3c5b0 100644
--- a/nextjs-app/__tests__/components/PlaceView.test.tsx
+++ b/nextjs-app/__tests__/components/PlaceView.test.tsx
@@ -68,3 +68,26 @@ describe('PlaceView', () => {
expect(screen.queryByTestId('local-vs-england')).not.toBeInTheDocument();
});
});
+
+describe('PlaceView structured data', () => {
+ function jsonLd() {
+ const { container } = render(