<!-- SOLID CITY · part 3682 -->
didate(args,ward,frac,side) {
  const {groundAt,isAllowed}=args;
  const p=samplePath(WARD_PATHS[ward],frac), halfW=4,halfD=5;
  const roadHalf=3.8, setback=3, front=roadHalf+setback;
  const cx=p.x+p.nx*side*(front+halfD),cz=p.z+p.nz*side*(front+halfD);
  // Local u = road tangent, local v = outward from road.
  const ux=p.tx,uz=p.tz,vx=p.nx*side,vz=p.nz*side;
  const world=(u,v)=>[cx+ux*u+vx*v,cz+uz*u+vz*v];
  const test=[];
  // Footprint and a 1 m clearance margin; 5x5 grid catches narrow exclusions.
  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 {ok:false,reason:'footprint mask',ward,frac,side};
  }
  // Entry approach is checked separately; mask alone does not guarantee accessibility.
  const entry=world(0,-halfD),road=world(0,-halfD-setback);
  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 {ok:false,reason:'entry corridor',ward,frac,side};
  }
  for(const [u,v] of gridPoints(halfW,halfD)){
    const [x,z]=world(u,v),h=groundAt(x,z);
    if(!Number.isFinite(h))return {ok:false,reason:'invalid terrain',ward,frac,side};
    test.push(h);
  }
  const lo=Math.min(...test),hi=Math.max(...test),floor=hi+0.22;
  if(hi-lo>1.2 || floor-lo>1.5)return {ok:false,reason:'foundation relief',ward,frac,side,relief:hi-lo};
  // Plot center is not automatically a lawful collision footprint; report polygon.
  return {ok:true,ward,frac,side,cx,cz,ux,uz,vx,vz,floor,lo,hi,
    footprint:[world(-4,-5),world(4,-5),world(4,5),world(-4,5)],entry,road};
}
function addBox(T,g,mat,x,y,z,w,h,d){
  const m=new T.Mesh(new T.BoxGeometry(w,h,d),mat);m.position.set(x,y,z);g.add(m);return m;
}
function buildHouse(T,plot,index){
  const g=new T.Group();g.name=`Ward ${plot.ward} House ${index+1}`;
  const wall=MAT(T,0x8d989d),roof=MAT(T,0x333d48),glass=MAT(T,0x577989),gold=MAT(T,0xa78b4c),post=MAT(T,0x667078);
  // Local +X follows path tangent, local +Z points away from road.
  const base=plot.floor;
  addBox(T,g,wall,-3.9,base+1.55,0,0.2,3.1,10);
  addBox(T,g,wall,3.9,base+1.55,0,0.2,3.1,10);
  addBox(T,g,wall,0,base+1.55,4.9,8,3.1,0.2);
  addBox(T,g,wall,-2.35,base+1.55,-4.9,3.3,3.1,0.2);
  addBox(T,g,wall,2.35,base+1.55,-4.9,3.3,3.1,0.2);
  addBox(T,g,wall,0,base+2.75,-4.9,1.4,0.7,0.2);
  addBox(T,g,wall,0,base-0.11,0,8,0.22,10);
  addBox(T,g,roof,0,base+3.24,0,8.7,0.28,10.7);
  addBox(T,g,gold,0,base+2.95,-5.04,8.05,0.12,0.10);
  addBox(T,g,glass,0,base+1.5,-5.06,1.45,2.35,0.09);
  addBox(T,g,glass,-2.5,base+1.85,-5.07,1.4,1.35,0.09);
  addBox(T,g,glass,2.5,base+1.85,-5.07,1.4,1.35,0.09);
  // Supports extend to the actual sampled ground at their positions.
  for(const x of [-3.55,3.55])for(const z of [-4.55,4.55]){
    const wx=plot.cx+plot.ux*x+plot.vx*z,wz=plot.cz+plot.uz*x+plot.vz*z;
    const y=plot.groundAt(wx,wz),height=base-y;
    addBox(T,g,post,x,y+height/2,z,0.42,height,0.42);
  }
  g.position.set(plot.cx,0,plot.cz);
  // Local +X = tangent; local +Z = outward normal.
  g.rotation.y=Math.atan2(plot.vx,plot.vz);
  // Local X may reverse relative to path tangent on the opposite side.
  if(Math.abs(Math.cos(g.rotation.y)-plot.vz)>1e-6)throw Error('rotation mismatch');
  return g;
}
export function createWardHouses({THREE,groundAt,isAllowed,ward=2,existingFootprints=[]}={}){
  if(!THREE||typeof groundAt!=='function'||typeof isAllowed!=='function')throw Error('THREE, groundAt, verified isAllowed required');
  if(!HOUSE_WARDS[ward])throw Error('Only wards 2–5 have proposed dwellings');
  const group=new THREE.Group();group.name=`Ward ${ward} Dwellings — Preview`;
  const accepted=[],refused=[],occupied=[...existingFootprints];
  const desired=HOUSE_WARDS[ward];
  // Proposals spaced along existing path; refusals are not auto-relocated.
  for(let i=0;i<desired;i++){
    const frac=(i+1)/(desired+1),side=i%2?1:-1;
    const p=candidate({groundAt,isAllowed},ward,frac,side);
    if(!p.ok){refused.push(p);continue;}
    // Conservative footprint-center distance; require 4m beyond 8x10 envelope.
    if(occupied.some(q=>hypot(q.cx-p.cx,q.cz-p.cz)<17)){
      refused.push({ok:false,reason:'house spacing',ward,frac,side});continue;
    }
    p.groundAt=groundAt;
    const mesh=buildHouse(THREE,p,i);group.add(mesh);
    accepted.push({ward,plot:p,mesh,stand:{kind:'horizontal-rectangle',center:[p.cx,p.floor,p.cz],width:8,depth:10,ux:p.ux,uz:p.uz,vx:p.vx,vz:p.vz},
      solids:{kind:'wall-meshes',source:mesh.children.slice(0,6),requiresEngineRegistration:true}});
    occupied.push(p);
  }
  return {group,accepted,refused,planned:desired};
}
export function measureWardHouse(house,groundAt){
  const p=house.plot, heights=[];
  for(const [x,z] of p.footprint)heights.push(groundAt(x,z));
  const stand=house.stand;
  return {ward:p.ward,floorY:p.floor,cornerRelief:Math.max(...heights)-Math.min(...heights),
    minSupport:p.