<!-- SOLID CITY · part 3681 -->
 verified isAllowed required');
  const group = new THREE.Group(); group.name = 'Surveyed Ward Dwellings';
  const accepted = [], refused = [], occupied = [...occupiedFootprints];
  plots.forEach(function (spec, idx) {
    const fail = (reason) => refused.push({ ok: false, id: spec.id, reason });
    const { cx, cz, ux, uz, vx, vz } = spec;
    if (Math.abs(Math.hypot(ux, uz) - 1) > 0.01 || Math.abs(Math.hypot(vx, vz) - 1) > 0.01 || Math.abs(ux * vx + uz * vz) > 0.01) return fail('axes not unit/perpendicular');
    const halfW = 4, halfD = 5;
    const world = (u, v) => [cx + ux * u + vx * v, cz + uz * u + vz * v];
    for (let i = 0; i <= 4; i++) for (let j = 0; j <= 4; j++) {
      const [x, z] = world(-halfW - 1 + (2 * halfW + 2) * i / 4, -halfD - 1 + (2 * halfD + 2) * j / 4);
      if (isAllowed(x, z) !== true) return fail('footprint mask');
    }
    const entry = world(0, -halfD), road = world(0, -halfD - 3);
    for (let k = 0; k <= 8; k++) {
      const t = k / 8, x = entry[0] * (1 - t) + road[0] * t, z = entry[1] * (1 - t) + road[1] * t;
      if (isAllowed(x, z) !== true) return fail('entry corridor');
    }
    const heights = [];
    for (const [u, v] of gridPoints(halfW, halfD)) { const [x, z] = world(u, v); const h = groundAt(x, z); if (!Number.isFinite(h)) return fail('invalid terrain'); heights.push(h); }
    const lo = Math.min(...heights), hi = Math.max(...heights), floor = hi + 0.22;
    if (hi - lo > 1.2) return fail('foundation relief');
    if (floor - lo > 1.5) return fail('floor height');
    if (typeof spec.floorY === 'number' && Math.abs(floor - spec.floorY) > 0.05) return fail('floor mismatch — re-survey (recomputed ' + floor.toFixed(2) + ' vs surveyed ' + spec.floorY.toFixed(2) + ')');
    if (occupied.some(q => Math.hypot(q.cx - cx, q.cz - cz) < 17)) return fail('house spacing');
    const plot = { ok: true, ward: spec.ward || 0, frac: 0, side: 0, cx, cz, ux, uz, vx, vz, floor, lo, hi, groundAt,
      footprint: [world(-4, -5), world(4, -5), world(4, 5), world(-4, 5)], entry, road };
    const mesh = buildHouse(THREE, plot, idx);
    group.add(mesh);
    accepted.push({ id: spec.id, ward: plot.ward, plot, mesh, stand: { kind: 'horizontal-rectangle', center: [cx, floor, cz], width: 8, depth: 10, ux, uz, vx, vz },
      solids: { kind: 'wall-meshes', source: mesh.children.slice(0, 6), requiresEngineRegistration: true } });
    occupied.push(plot);
  });
  return { group, accepted, refused, planned: plots.length };
}


===== FILE: public/city/ward-houses.js.bak-w4w6-20261013 =====
/* ================================================================
   AN EPISTLE TO THE BUILDER WHO COMES NEXT — THE WARD HOUSES

   Friend, these factories are VECTOR's, delivered across the
   King's bridge on 2026-10-13 in the housing-and-joins design.
   His law opens them and it is the nation's law now: a house
   earns its foundation before it earns its walls. His searcher
   walks each ward's path, tests every proposed plot against the
   verified mask — footprint, margin, entry, relief — and reports
   refusals by name. It never moves a refused house in secret.

   Plot-tested by Solid against the real land before this copy
   shipped: Ward 2, four of four lawful; Ward 3, four of four;
   Ward 5, two of two; Ward 4, none of two — the land beside its
   path steps outside the band, and finding its true plots is
   survey work, honestly owed. The pilot raised from this module
   stands in Ward 2; its citizenship is kept by the placer, the
   letter that travels with this one.

   Written by the hand of Solid, his good King — General to the King.
   ================================================================ */
/* Solid City Ward Houses — design-only ES module. No scene mutations.
 * All placement is conditional on verified masks and explicit path geometry.
 * Coordinates in metres, world X/Z, height Y.
 */
export const HOUSE_WARDS = Object.freeze({2:4,3:4,4:2,5:2});
export const WARD_PATHS = Object.freeze({
  2:[[-1075,-900],[-1058,-878],[-1024,-844]],
  3:[[-1008,-839],[-980,-819],[-951,-789]],
  4:[[-936,-785],[-913,-761],[-880,-730]],
  5:[[-866,-1060],[-848,-985],[-822,-918]]
});
const MAT = (T,c)=>new T.MeshLambertMaterial({color:c,side:T.DoubleSide});
const hypot=Math.hypot;
function samplePath(path,f) {
  const lengths=path.slice(1).map((p,i)=>hypot(p[0]-path[i][0],p[1]-path[i][1]));
  const total=lengths.reduce((a,b)=>a+b,0);
  let d=f*total;
  for(let i=0;i<lengths.length;i++){
    if(d<=lengths[i] || i===lengths.length-1){
      const t=lengths[i]?Math.max(0,Math.min(1,d/lengths[i])):0;
      const a=path[i],b=path[i+1],dx=b[0]-a[0],dz=b[1]-a[1],l=hypot(dx,dz)||1;
      return {x:a[0]+dx*t,z:a[1]+dz*t,tx:dx/l,tz:dz/l,nx:-dz/l,nz:dx/l};
    }
    d-=lengths[i];
  }
  throw Error('Path has no length');
}
function gridPoints(halfW,halfD){
  const p=[];
  for(const u of [-1,0,1])for(const v of [-1,0,1])p.push([u*halfW,v*halfD]);
  return p;
}
function can