From 10335268122a187ce1325098724c8ba461bb782b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 9 Jun 2026 16:41:00 +0000 Subject: [PATCH] =?UTF-8?q?fix(elmhurst-extractor):=20read=20Main=20Proper?= =?UTF-8?q?ty=20age=20band=20from=20=C2=A73.0=20Date=20Built=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Elmhurst Summary §3.0 "Date Built" lodges the per-building-part age bands; the Main row reads "Main Property" / "C 1930-1949". But "Main Property" ALSO heads the §4.0 Dimensions table, so the global `_str_val("Main Property")` collides with it: when pdftotext renders "3.0 Date Built:" glued onto its "Main Property" row token on one layout line (as the recommendation worksheets do), the first standalone "Main Property" match is the §4 dimensions header — returning its next token "Floor" as the "age band". That garbage age propagated to `u_roof`: for a "Pitched, sloping ceiling" (PS) roof with no lodged insulation thickness, `u_roof` returns the spec uninsulated U=2.3 for the correct age C but U=0.4 for the unparseable "Floor" — collapsing the roof heat-loss term and inflating SAP by ~14 points on the affected cert. Scope the read to the Date-Built block (between "3.0 Date Built" and "4.0 Dimensions") and take the first age row — a line beginning with a single A-M band letter + space ("C 1930-1949", "A before 1900", "J 2003-2006"). Building-part name rows never start that way, and the Main row precedes any extension / room-in-roof rows. Regression: full sap10_calculator + documents_parser suite green bar the 3 pre-existing unrelated fails (2 stone-wall U tests, test_total_floor_ area); the multi-bp / "A before 1900" fixtures (000516, 001431_case*, 6035) keep their age bands. Co-Authored-By: Claude Opus 4.8 --- .../documents_parser/elmhurst_extractor.py | 21 +++++++++++++++++- .../fixtures/Summary_001431_topfloor_flat.pdf | Bin 0 -> 77066 bytes .../tests/test_summary_pdf_mapper_chain.py | 18 +++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 backend/documents_parser/tests/fixtures/Summary_001431_topfloor_flat.pdf diff --git a/backend/documents_parser/elmhurst_extractor.py b/backend/documents_parser/elmhurst_extractor.py index 01b50deb..e2a5f1e7 100644 --- a/backend/documents_parser/elmhurst_extractor.py +++ b/backend/documents_parser/elmhurst_extractor.py @@ -1727,6 +1727,25 @@ class ElmhurstSiteNotesExtractor: )) return arrays + def _extract_main_age_band(self) -> str: + """Read the Main Property age band from the §3.0 Date Built block. + + "Main Property" also heads the §4 Dimensions table, so a global + `_str_val("Main Property")` collides with it: when the layout + glues "3.0 Date Built:" onto the "Main Property" row token (the + recommendation worksheets do), the first standalone "Main + Property" match is the dimensions header — yielding its next + token ("Floor") instead of the age band. Scope the read to the + Date-Built block and take the first age row — a line beginning + with a single A-M band letter + space (e.g. "C 1930-1949", + "A before 1900", "J 2003-2006"). Building-part name rows + ("Main Property", "1st Extension", "Main Prop. Room(s) in + Roof") never start that way, and the Main row precedes any + extension / room-in-roof rows.""" + block = self._between("3.0 Date Built", "4.0 Dimensions") + m = re.search(r"^([A-M] .+)$", block, re.MULTILINE) + return " ".join(m.group(1).split()) if m else "" + def extract(self) -> ElmhurstSiteNotes: emissions_raw = self._next_val("Emissions (t/year)") co2 = float(emissions_raw.split()[0]) if emissions_raw else 0.0 @@ -1744,7 +1763,7 @@ class ElmhurstSiteNotesExtractor: number_of_storeys=self._int_val("Storeys"), habitable_rooms=self._int_val("Habitable Rooms"), heated_habitable_rooms=self._int_val("Heated Habitable Rooms"), - construction_age_band=self._str_val("Main Property"), + construction_age_band=self._extract_main_age_band(), dimensions=self._extract_dimensions(), has_conservatory=self._bool_val("Is there a conservatory?"), walls=self._extract_walls(), diff --git a/backend/documents_parser/tests/fixtures/Summary_001431_topfloor_flat.pdf b/backend/documents_parser/tests/fixtures/Summary_001431_topfloor_flat.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e61f3466a02fe2e390858a38b4a9f1ee04e34c77 GIT binary patch literal 77066 zcmeF)1ymeCqA=7MTDs+ys?s=AtwqEHkSr)OqhLu4jpBDK~t}w;~t4PG$ z=b&5+lyQ9Da|v`rw?JRB-3ucc=<3*W>HhFhP*}JE)qsdl&*6zMZqOSfqO9jcTTL3z zzD!P-6;3o}f;BdjR3~@cJfEIvm?$d!cB!gnM3mf{>-QgC?JHgKy!Wmf=f=vc z*7fA5&4gh=iNc!<@9R7u?9q}9`vlyCR-c6N4u8B*3dRNpH|&4oCR7_}21nOAoTc_l zbYhKnq0A2-k6<*cm2)c9k|)P z3Yj~;%A_^lXkOge@eHkW=o!YHlJFdwn@pO~v!@HF2YoSIx&=-&DD z>$ty@MbnKc-}6L5f9`AZ4S2{KVto`oF}b3cb!^%^5GA1Yv%~2nb6$?&M@g?^6)B3V zgx%S94L2VmJMR@$DD9uyYn5SERWYIUQ8+||Z|0Vw2Emm0wX=}$)0awW&i-`dix0iF zp9r^ zq-dem=fI^^b7S%8#mM{vZ)m5Lh$Z>F!d_&j>EO->yAA z8f%j_FgK_y70qRSX-?U@;; zUlbwfw=vTzZf_k3WtL24nA~TM>#t4QGk7a`iRR|`U0ANq8{SRXfDS!U9rBA2J!3jG zXau|OuG?@nBp$RJ+aF5KOG+5OS*vq=$vcC^E}^sT}7#ffWP(^tlDO14Y*Qw*YoMGhQCfh-=H zqaD!pNyymJE`igk50!Ik(OA%ak{kJ}RJtZp=88Idn>aRW#`TC0MW&N$Kj~h22-k5YdlrpV`=|NBEdk=VnS^8lteo zeLNHM5A4msUu0&kJsiqk)QykrXs}f^;)kw~FwLA@5OX{Cg2i09*j67r1)5Y0tc%MV zu8%yt=C(dOoW0!>jjp4jzp`v+NqS?%JE!jg|RIZGC8@C+5T|d z&~wtKg9Gn4<3v>dAu0H{v?T^)-%n>@c;V|#ARVZMvp^x9O>ytAFDO_P3+it!Py?|V z6{|i=040gu@*I0T*jM$IeW{p!fEj)NkfB5F9Z_i#@Aom3gfx1A1?9pnlq`!H{K3pX*&}i zqu{g9dpMptjIH4uscOgGujYIiwfJF#me8O#*zy~P&aKhj-T zXNBMADi0|c8MVEO%tAV{&4LqC0Otn)9;JqzpdzU+C<&HsTz8q zy#u=zoc4*T$l&y2mVJ*fZNtL6`I=7=8}*}4=O*7+8TaPk6ww!t(uROBe~`|Ju?2e) zohV>tCPRhH8ebw__VDv?;XJxLu;1 z+g;Rn+)qD3{|a2}lD5qs8VnND?H5t@I%8cI|3!4S;041L-A7W{k3U_=l$Vl-0Lj;3 z*lm7+MDC|%gtdJvO_D)oP3JSnTYJ_QM5}o2PMHs9wE0fF`=Pp&3E?6rNQghsaEP*I>& zFwa3<^B_p`bi6KuzM;+H>?ZR}CK7ase8XkpmEdg4B3L!VS}dMp@w?c_vFf7AyJWJu z%J(G124X!@(Z*z-Jy&QfKBDUF>=Dh4Iw&7-Pj!cUGXy)FZ`6gSgE}-6%EsHw#Ka2! zN?kyNPU+C#9sC!2|KcMv=88Vtq0;xSXJo}Wxp5dMATjQ{rN^t2-3ja@X>zvY7_s#y zebIgiS>EYnE}jOGxWm2EX9KCCfeqKi*|QQP5nV~I>%U67oP&i7SW~19eYC6VGiTXW zpp9sz-F;~?CcnGQo<{jFX&*UcaX=2&-Jp$b+${b{k(T=G+r1x{4l2TGEz`FpzXx7Y zjI(E+)BnQG98N~Lo$pTkS=o1;!Jo-t6aex;4Q;|VXe&aJySzgpNzoixst`bikR(G} zZ-r(Jj+tj<)|%nzvs+quG0h8uOK#o>qi$)Hg z?M*?tF0GHLP3IcO^5`0^6G1IqYI~n`m+825SBwHw$lB{f$6AWePEK^ioWc$k9OIRD zmyg=NL_^y_Xd`%d-?=ZiyT2E&P|%&<25+=^b~|jmIbN!%R-9pD5Px^w=dagn5Y6I4 zyT_mzcr$d!^6WTd!3!l>f#03X!tMTL$+Jva$@HH2T zEUgO+nt{UnJq@2okJ#2(JxZ|9F8bF!izEDMLmI}X<>opU&hihNUCn1xj$%KLr2|Xl zUwf+ChYk)%2gay$OMlrRoIuq{cEO44ra9GtEL|{@!^l=hddN9(ePGzc{Lp?n^D``F z5PIVF5Mz;W1Jj4-`)(R_jue-?JtXK)=!Q@%biYF#LxVE?tKNj9Ol4GAuj44lQBInp zn+|sGcaZ>g{5DZr#&TXq#-=~)zCEJMrs@NmzFlC0zV`S2{?X|Rg=!{Dgxgn>b%h45 z=W7JjAmX0mXGf8B;*IE?Y&H!_Ee9t-$j(7>mpLMfRTeirFV~;{%0E6*Qg#1IL)Blj z;dq{1kP+c!kFO&wF!q+}_lrwZ=cY@6(0)HJ^QwuyouajP)C&)uGeS>IvgpY+_N%N+ zg!e9;88tGmd&S}1>)6BXllD{!($4G~#>(}%?)o#h^w2o2zJ8yw#$zkeNZ?0fu=%?E zEH7fzwpMTwPiI6b>Isi9qjZz*zDsAe$(+&Q~~11IW4jd1ECw8 z>jl9~$I{V(=Zs55bm?n3WbT%dg!6du&I^s%XP=b<>& znaoX}z}L<46j{3-rCb58i@fXAhrP^ie^?~8PWm;455&pi#wQXrXy-kLgAL=quxpgW zOG%M~M>gZBS4dR*#m@2YGa;~kWSTyY6DcX7AZ&{;Oz%kD!MaWJJ-4PN6^qAv&!Vyo z-C2RnC+gKN)nV;Fa_X`?OSfoPf}thBMCSPQy)gM8mX)IX<+F z6@DRQK(B=b-sds}lmQ9O!q_CTSavZaL6{XHC781!!KbtG^l1FAqkFyaFyrB z5vX*6-1^PsI9Rw6ZAD?LG!ELnPIPR&e%@M_~?UHzyLF8q3Q?(^WK?cMFb zh~PbuL*eDpM4CqsS+{)(GRkc<?DZBl>y&aFAZO}trHm({u!XYbyfLF!O>>VNTQJsF`${c~UW3CY2TV&H#!Ct&5 zG{{K!Nvtx(6hEYx;{3)0%r9Q>`JGTJk`?c^&?Z&GjfsQf`x;zq=i7((Q!jkxIZnv) zu)HuT;r%hcbM)htc@;e zXSzBd6FY%+l!3Y!v!hZ;cr}ZcoW_{MJ;L{Nhm~EMR&=8iYjxrfNoMqD#!%IHqM1PZ z*hpkNMgUj6EXp_o+NV~CFYqziGT$M4JxcZ_)7v>ug*SI`azSvk@^YB=Tl^*)xol3P ze{NEZIyjosf+IKIX0%XNx7YJ~u&>2z4RqT}gri3}6w^)!-C|+;k zSg;|xyz7G2lc*LH44Oh#iLSg}_YzRtm8HnRS#?Cj$&GX~i;&8WkuG*1#aBL2wJo6Q z%Y7}#Ubw#(I24)Ri$H$nbg8^G(B^O8>R9J6EJvrwhZ$46`yLA-d&7l|DV71bb9FRW za{c^dri7m|&B9+8q+$oRigS)ohdtZ0Sd;`mx}rEl!^+(KWA)on$R5qK#5td*bT1BA zc*Tw+hE>FqpL}EH=iHlgFUKZ9FUf?@d!G^sM@sDZu|9(mZoqFLJ$H<$&2wuw=9wuj z6Z#+U@}k69ARd$WCN-Yqh3Q~ENb*kMExPNj?~iE57{T4_4&DBqo({rTv8!o`(xLg5^ty6aV$Fs2k&$gjH(qBn$zO>P;`t|BZqGCZL z$&-btVn^L>PgFO!Ae>h|YHF!bJzS%f5NQYZZ1q|6F-v@AX&a#KK`hYhcF_9QU zDA~mqF-3)JN^Eunv%oFQYcUVV#Gu?U69uHrsR0pwJ$^cZ?0wpi6Rj!p)8x@i3Dn8H zjv{<(pDkJSiTM*mDXKG8Dc;U8GRmAr_vlEaawHxfN>o@N%65*$aV~b6O*w+A^V<$(HJI7U1JP{ z>7P--&&X>LIx`|+{S|Gcoz7LD|3NErsA#>ZvGC0-_Z_r7Sc#t|`RQZ(HPMeTdo!-_ zJpYV8$U3}Gk@2^vSMWU5eM1{>-YH@W^@t3r>x!14dut|0iV+;P8W>@LrNM5f107n> z6X>VnYkeV-Gr(i_c+0}(C(;{qOzvwXk~@qq{8?`~GAPj{b})9jf=cNJk^amNVe@M` zeuiAcE}9+W$>43`%C;wQ_6K~&2#zU|f*2*+TfT6xbiEQAi^1LRv8ay{o>Gh=pp!a8 zJ^%63*-6l^b%uMKoYrl46M{q+eZ9yZha5{%IacpBTuaE=zuR0v5Pq<~e<0Ax>nQsj z9s!$Kammik<_NxowandKY8xihPlR-y4vD-?-CA23#DrYT;^ZX3rxnwTY~zwyFR}5!_NQx7Jo6xPhlhAIlU1n6C^6=@$^O zc)pIT&n$rdO9v-te&yk*Bx?N=MCcIrUxtDIGT6=buMJN_N2>q1;c4c7Fg(r4#?JJg zhNs~jUQNYMWLVC`$yK;Fa0fe2HS)-(8AQfTEA4Sv#mrOmC(x@0G+rXAl{vr1u172s zF6S)sX~cT-p#=^ThDWIEbMAC~S>j8Cr%#bzjNyi|UEcbMpxzHWu-ZSkU*5q*e|`pw zh)u1SCFt|_8*P5U$$6TgMXWs9NQh_zTz50-d}>H^YdqQ*h-j#4iK5S+iqP**k{pIx@e%)-LLN4|Q7JHK^RC%GKg zB9J^`&e1edIewm97F1qV#>BMN5!}qy{h-+)cadOM3nBTpl#YPMY3fk-vMd5?R z(-fd3b}+h7-@KSvWi4Gx&3-!7iSrG~w~ex_rsf3|*IPKPmFXiDmHys!V#gX`qf8Vf zD*^iQQ?~WVv#}9*P(oWq>)F|lX@pW;6BiRl;SQ727v^{7Gg0l=E?AtXl_n= zbjwdAwk@_yPbYPN7wiK9Ho2@nYkvu6s~oFyh>S85Kpt`R1AG37k(i_fQU=B?(DqPqOQJ$Vi%FImUQ1hq5Uck+(s=MN7(%@cC z9XFtiwH5Uqu8pSV3_JVX z-Cfq?)>Z*>WxjRE8KNr(&VzmWWqi${_N2kanL6l2C!nUX zGMmjIPtL@V?9YGXlQX0~nmrS6oY%i15eZb&)F`72WqdgTZIq|_4 zd`CW=r(Sc2c-i5!_{;@XNnQOz_OHwOx4aZ6?B{fe3`>W;O%l*@BPeYBJsdV*877tB zRWMA1EdhaBryWvN=*X!}fM*-(X@x|aCjTe5#IwfDGmTbX3THD-5bsWlTUYHh>>G*6 z@3R3n)9swq`?xJdEuZX%BIIAUzrL(5PWXk3C9;==aarzQ20WZ;8vxLE978mn!SK)v20TeKi??vG?_Br_hKe>SEe;!>7{iY2>UzRF05$*%tKuL&3{>{eBfx6! zyvjE3=A<^c5t?XIIh~Xmj}y zPrx;XrwadWMne4V?e>PC(JL9cN>*dbl43rsg*{UzjU9r5mVg%09wP(E&k9$2ISNGk zgd=fBY-f&VVJKluch|X)#Nl7yF3R${y4l&WXU1RCKxU_IsA2lXsUd;+MlAzX3*K*-xli#e=Qd-_tfjYPGh!Ls<%h|J=k`qLrQ>TiNk7c z;0(EEa(v-A(i3TScefSnUUmdhZDwq^SIivhfz$P3uuqLMGBmZkwosFQ_7_Ldhf>H{saih}Cl zjz9J(8hd0@mZkL|6O*I8)fBQKzS>@2cG(!)2&p((ZOh&pcw6;zyq@HPI+&6xCk=Gz zp{H-$Zey^I(B9`=QR1P+T^N=2fw7M5UMc8U)*bRuxzab7iIu}gduLCD=`KGfr@gCh zwsnZuE1XNXiw^yH_sY2uUi*;!A6+MjwaiyE-V%E`mQW^~tnOkXE;G1N6f zMb)bHnCWRLlPL{UR#wIvf?p}B7-Q*er9bT(=)jg>fPwwu)AQof$TGVA^L7I6z?Ry} ziwj|4$q1G2-*h`(ZX;9Vc{jJbda9eXtFof1{Q7INYsz~PldkJ%`}5O2sY)Y=S>~mw z>o3lP;onr?C6$sE_$yL1RUGxefiUFo?>pSw+%ffvJ#;UKr%~wak=m$xpUb_<7#a!; z#QVzhoHYo6KQ}L5I^q+^2x0?Z31-b@Q6=t?qgR=>9eX2=M z4_-D=GH}S-!88a9*+@x)v1*>W!_w1pSFnQ(eLzmAik^mfc`V6fABQrL{5M3ka@JE| zsxm>&`|*7ZW~LS!oG38m0?fvD4zKmk(~QT*Gk<&4Eba9#B_9}{?sArfzjUp2)gVZj zQJ*0%X9CCgNj3!g2c_~@U|nFbL z&!qO^Gn>8gce+G~WQ0XBh?Ges)8oZk%sC}(OCnyF3URX5gH4S$O+Krr>+U-vD%5u*o@$$yLdh$%YBUtwuw5k4S@lIcU=sDVez9k*qSAMyi`Fu% z+h^`ah;1(+k#9wK!oCI$A$=aiNA5)?S`IgRGDL~C3->Rqmpjzz)5!aQc8 zqod^*azis{2nqZAqx{Ft?*3~Yj0WLPib^W72|i(70Ih0@m#N8#dHg>USijC`W(K9^A%E5$)f*MU&SXekZKDzMhH!?L_Y->SbDIBDQ;mAV-Z2QW!Bz&Kr z9lluofufx*vg@l*$z^)P?cjxsuZpPK%(4Qbd;}rRuz1-Oz zPfv@Nv;y;3--1Ca1d!Y3geFd&p1agP++6}b4z>*t?r&V|&z=0nI3|OMsK+7f*ao!hLwtnhYrbGkea;jNxg=q zMvu$HoC8nweF*HXv3KU1cDL5=+om@siA$9at3r!8Hu*KP>kY?VOjV%K&b35zMoRwIlHHA!;3)|QuBAhEN~Y9p zQ`6tBuA+U0EUl#t^YbAmb|a}_+WXzWgD*7&(_{0(cf~2Gq2_yosoXQ%_+iLL-D3l{dxhgqo4ibtVg?GnC6^;u zJX=if@PUJaS=DuNc8tYomDKmJ-{YDVOL+Qrj4Ov2AD2u!YPOIykv!g1jF*|7J$wX- zYz%&PXGf4jQo)YDP~g7a{EMRb1rK>@K0>yekJES-Hljogfp{8da>>ls$n{Sg`YUvnxfs3qGkf2Qm*}*7Wl}k=dieGwl)QE3C zKHjcON=kIjKu?o5Gxk9Vq6DX%uFc?p*TCM3K^(ccw;rZJQ^+x7XKxz|Qg#W98n91kEKk)Xx-4% z86ft1AWMxt6@_l+U?))dfD`hmFC+vRP9y~+>N`O`t*W6U{C1)>wib`0RH}F&5S?{0 z5SE5rW_lyE+;j%P`rAiaT_G;&z3l@ZiTV-~QITiU7qzwA z6VH41yLb9#)m}1vY2<5EUKJ`hj7Z+z_PO`c!lE~^)m1v~Jgs`GFz%))g!@ z4XcXgr>3MeP+I7dOZAT89l^~^8p{?P!LBXI0oEjJ^Ig%z@g*VlOS(3&=c<#3oBZ(= zV*orR^cVe13kwYzV&0S?6&935ZN-^vp44fI3r|bXfAij3IQf}~$ItcgB3xKM;LNzl zInpIPO(&NF&h>NIk01U_=nU2(W@H$;`np0-i@w=qxkikxo6p^%iEl$1O^?rSXI2YJZNG^2u$P*6|`R~O+_3n+9!s$R-qLtlwGMu(V(BfL;ZuVld4x8A4e zWtJKjV!D?f4GiA!(W59yi-8y7!Q8L`GlrYHOKfxxI6bGOjTY7MZyIwSByL_-r#HzU z@_afuu?tj@NC+vU{YqaUJXp1@6Pdwto3LRUkV~HbQM<3X%9etf*JkVR5QK>na}v@U zPJd#%a;La|?epA-3j1V-@9X1ZO>u5#`!y}hfq{WD2WRJ_cY*$G6RN82dx?n&FwdW- zdw6;4EcqgiR!h#$e7==jvqO5)`{^T*ZoS6+a3_CBchr}p#`^iZ~Nc8=; zm$Z|{&e_F3uzkMl&>|JSN$ecZzpCQZPr6&xDQQ-sUbuu7iYPA!3%Bu9JV$5epg@1j zUF=)WyDj-4$vldR*haC{-h^a){Ej92@85$Fg5@tF803e*#9*$@$yrLj=r6gXPIBRp z>d5aoSp3n1dwa(;jkmEaW2>v3Jw20?^+*DAeSLfjd}odh!4zIFFwcY}ZC(s^O@<%r zZ2wv@Ib2?qr3wt8q@*N&O)lPPucfZ4TJtmTrCx}VuiS^liv04I3d27q!Wki1}0lovC#C=)Xlz|f`E5-_WXN=N8UN@s@dvB zf64H2F-emBwT#r-A(X_9q-g3M*CXA+kl z&qw_-O(#j`rkuC+qn1T+LLD4&fWxofzg=bQLp3gD6V7$XzKFvGCWnQEgoOC`)O-G# zC^oZ1DfxW#f?Qgq>yG6bMe3j9lZB9_kF(y2%fIdK zpQd^`)T5^b2Ih8j3?>z8smZ&mBo}J;HxnbhyUNeAP3fpUXBF26}Q1gdTE|wnCtRkvrx6b$j-yaRRS)omZkezPK{}D&pbPt*ChsVl=Wug}8PYFP+&trbo60n|aS-$ujeIfONw8nUM9fJf9jU&dhev=8>>XCs|a@B0D z#K6R?SsR9T#hZ&2?-=OIKNc^;u#1Fos6OSy1L51hLF2XAMu<{RPKMqt(!AMKK*-I{ z%}A_3YZqxZ>dHr1Cn9;TSqP`4*I(BOVq@VU&!9*gz&_9gTdmaEi5By~O40D2pP z0C_^fcz9LA6_C6VWja~aDVA&g?t*-z2L08lVWD`*t+bK7=wPBzaVD|dotE3l1lcv7 zf_XCmsC3VTWhL})^Q=o6iU~uIuw#a`)RaOtN!QFsp0w1i zxqcHbng5n{F^%cmJxSt)f7_J1c9(S;Euh|H|Jlnyd0c-)cspX~^GLmW9gpM_A=GjI zBe|1l-6hiIcpdyci4@T2`%yAHU$WjFX`c&mn;`Tj+`-CxYNF(BCLTnm&8&^q27Hp4UNPjG2apvL*w}ts~9bQNb&tI*1jn`xDmi6N) znB8$iVbm20Bb^}aS(;{89auTwIpcu>5NJC)d+~J>4WyAB+~elEh*+LLa`ov%Qyi#_ zwdACPuKE@y8R$+AhE}qk%G1!kjca!Kr1M0!-~>^yynV8F03IupnlU-8W07*qU6=1; z4IgvsK(Eh=#;y{nF-R9|Ying_Gw#LrYt6e-eYf-7md6>iU$a=j!^=`z+X#7&tHQjv z46Rw;AXtS8DOU&<%)M!Lb14*C>Y;$GHbAB*>ciTER1$eZ$$?3ZSzN2XVbX zbSz#HezSA3f#efXP44;;+WM#r>QPX>p;kRHZ$ue=OLUr&>9KV&vt*(}?8JSM`^);- zm>;Jm;lS&kdq%^v&mib)ujl7fXkZ}I7FD+(T{5y)T?Bl5eDbIfCu&=_wWnjTE3Jm2 zg($BaE%{!4n)h8HS396K0O5RR|1v#X{+p`XchS55Bz5ZTqSw^%c<+pyLU6I+)U3t~ z_58LlLRCwVe~pXDSGo9Xf6#Hc?JJHTXeDSaL-#u>7dr5_qs`*Ume}iGy`a*0!A5E{ zTQk2`T)f^Py5(B?G@aInxIOb`AUU=9A@p}J6`M{l4VgB>R~XiDRyEv&zQnYCeWV66 zGYd8jHUiQ^{+J+4FuNUBeix;m^yR*+f=o4==F~9hQ(+vbC4NzD%HL{l&}g$fprd7o zNQg?YvC7IyADa$Pw$>9_xuetC-CQG^vd$B%k~$MYKSzdsuPRfrElX;_b)mqt7Nl)t za1o(iiI<$;&$lV~-12iBTd%iLY$n7dK0X@ZU2w-P%Q8%NclCMDM<=v{`C7j!bsbP* zVk(kT-Ww`orm=ojO^6x+zRK;#=KcCgB1_GttDT*1>EV{2GBaa%pWoe$&7nenpy<5N z;jQF#kuhgA5OlZ*cXHQ>1?+=KV76nB!14ACb_5IgTMxz2Yf^LsA7gJFZ!L?xyq?fL z>vi47HZ4lAXTy%JufMtHOi+1L~wjg-yc&y)_$%&3pFwEfhoD1O=CJE%KF{R>A? zRXwjT-bO~4h~*h(M!k( z6-zW;AOdfHe;ang4>T-kyuP_%%*65$TulG={g1Qh%iG(A!sy_$z#k`D+uLw_7A3{D zgU|u+MdFMjbI}4Wr`?25R&A!NB#?-RNEck-tHn-{(7M89PT|W=Y%9GSG77==p!tA> z7l^!(#;D%irG?R%RWN?FD>V;UAIrXEIQTsS^DrJ5y)A$ZS;g}1x0b1|sAwxSDmp2` zMMu~$+<1{zq+Z` zxh2V~jwGM^P4tUC1;IQ)p*I%}hK7b6)-!^cBGR>E+N7P(?$Nq;I`r87Zn$weB>AjMehII1IWL7BpLVsPoA9V z|Mw4XAF0uQhb?0H2M=!nTLjpm|4+FQV2c1-1lS_L76G;hutk6^0&EdrivU{$*do9d z0k#ORMSv{=Y!P6K09ypuqW?eIB94D;c>15VMXdi|cp9)pfGq-S5nzh|TLjo5z!m|v z2(U$fEdp#2V2c1-1lS_L76G;hutk6^0&EdrivU{$*do9d0k#ORMSv{=Y!P6K{*Sjs zk5e4|ci1AffAH`Yutk6^0&Ed5ZxJwWkv%YP5ioBNFmDksZxJwW5ioBNFmDksZ_$7G z3;&OAfq9F7d5eH~i-38HfO(66d5eH~i-38H{zuPS#PzQYPyf@li2WZ7PXo3Hutk6^ z0&EdrivU{$*do9d0k#ORMSv{=Y!P6K09ypuBES{_wg|9AfGq-S5nzh|TLjo5z!m|v z2(U$fEdp%O|M9kn`(Jx_%gV+mVr^k zJ`La^02cwc2*5=EE&^~7fQtZJ1mGe77Xi2kz(oKq0&o$4ivU~%;35DQ0k{ajMF1`W za1nru09*v%A^;ZwxCp>S|HtDZX6AqG>FqzQi@5&5(_26n0lEm#MSv~>bP=G709^zI zbP=G709^#=B0v`bx(LukfG+Z?_?f*UBtELsPh6QJuy)K9;tS{^KobP=G709^#=B0v`bx(LukfGz@b5ul3zT?FVN zKocmR<{lEVGu5?tD{O6Hz$k8^Yiv)-%JS!+u&}j@COsP`Hz_?QCmSg<8ygcTGZPbs zHb1|im4UUM86q>=->1N+?5OA9YGcT#V&`b6^0@IQ5pgC)VO@K}$3z+bLxN&f`ql=f zR>q7VQ!61Wd(*$Y7dN%DcMvhrwR_BstnOd7nORt%Aq^Z%>@`_9xkw)uR(5t$c6N?G z3l|qRDF+89DKj@SDJv`Mzb(*x>|AXB78bhiujl>sod4DOZ{eYFJjU~9fu_gI#LWID zvBv~CSy=uokJ}tj)x`hzpv5KVhN!|D5-iaDO`wdd}lEHyhjE z;(ml zd3`(&y8W2P$Ms(p?LXz=-^&gu^JCFc`uB3c{`ckJ-$VbGlJ-v*GLFBG{Qs$tG5?oB z#`w4L^#8JaadH6V>%YHzLCZ4q0_9-(vwpn59{;NC|K7jH()^#UkiT9L&{Q5v`QtH8 zR!&k@&c9qa&{Y4rkDZ-`l#Pk$?@Rq-G5-&xo)h{OS`G5@GXAOO6m^Xa85MQy9;bDC z{KXk1Mma+RQ{Bf}^zpN>bF(wBa6*&jVq<22)>K^D&?(}q9H61?NtwC+gjX`Ow|2DC zH?)UF`B+mvuAp`9Pvb!?gZPekZOjJh-Ibl>t(c$(t?-?J-=qUPN{mk{W4&;M* zeB@txaKHIKQ`o9%!Mk*z0^!CU%28;E|^T;!*7!Lzq;%Q{Ni9J~n-el39 z11U^!PR;$LBHIhphg|qrqltaS=i(XgY@b&}F$vz3!QC&@Y|tSsOUqVka1juqwDyct zc?229F-Zl+KE->x%nNt9%7vcR)_ojQ-|11h7o&xZrN8;DPxjdM>T=nkWx1)*i>aWGdq*`D`rGed$(SZ~%7viYCM6GBRGh}B zeWlF8C~1BtxT@|Y8Rrt~6Yhj;fx&$>_mQINBPX6s^()K#6ALfJ?FzG_S7N{PHl!Ne z_+R8WB0!oq{Oa#sb*?7txahWyhRl05k>Q#ZCaqTWvDyl^6L|J|1Ti)iSlR=M)@wibWVyLWG;=iavxBC9(Ic{Jsi`W;L0o}aOBEmFq z0}X6OfHJzO2O{ss#Am#&i9VceBZe){Gw1~=BPw{k+pFA zao~q2DO{Ggj#X*IU}+b=WpuuoHRO`!r6H-ux);%Oo>ZLi3^2lO$9E6N}7nng9KhYBw zP3GEi^Ep?-_(c^{yw_|Q{;6&|zJoo*Z;_2!upV`+yyVbew!d#jl^=f`)%qJcIz?%e z!9%_ufp_VoA3pzTAAK@kyJG;1lE1{u-N5G-%8n z!DrPTx&1L=qs7EY`6%YrbNe6G5Ds^-x;uM!4LR{#Z}5|*#@g~@!;nxCpOUOp}>3A zz4`W*%(b+)#nJ~4!wM;BNwWYmV)GQ)1nGIXrjol)xkx?vz6WK%(XTy>D3pv*Aq&wccm^LAzZS~c!lZ1p3zqbs*=g!`}h+09MaazIa9M7Ij zymESmuGxe|+sv2h(_reD+@gci_?aBa? z%UBW7bBl5Xq1%t|-clx6oogEwgAefQZ?oQgIP6*dP@}~t076sGBes*X3QvAL^RA|& zjCr)ebi=B|Ioe6hO-j3uT89xYip6<6I!7$w@IB-38mdQLMK)g>wE)i^vui!ZEQVpP`ZrM<({ZoGL_(j|x5143aS>pjqPPV{<5@_`b~*n~C-{ z{H^`LN1KvPX8Z&zdXEf`4~-oozd}X4uZk|!5lca2)N`ME;)Mm6Om9z~y_=eGn55I! z>JjWyY?C@nvoyEw_KtH9Q1WkT?D zwQN4$p_9pWqfqoGE$b#NnY~xEp+8KP=c&~n@h>^X%iCQpp$rM*WfizIVqg66ok!); z?wIB0dG)>Gg1e{)hEPE%y|DJ{0X}NB7JYq7j>Xq~Ub4gov)D-qX|>J)aCZ8A7VIr; zF04;=5aub-YJIpNzsoceJ*ap?MNUN(K_RCdjqliMZqzwaCM303_}+Hgl-Fo4vH4q; zQS+k>XKM#_t4{Q{f4~2QIxO?#_9CH8VNT(trgqeGO0A0BhZ{J~``2H8dsLN?T7Eq@ zajo`G_5jE9TV&K;j5NM)!$b1EV@Wn(D)y$YK$PF6vI9ly3>H&=Nu>MU)=2p&TfmDv zF%{k{)@eg zSe6>}hO@ybHqEJXaVPhS=or$_S0Of1eIYlI*No1B>gz2mu!`KSpG8hnt(jPtkOkL= zr9^{&V~u^n5|T>mBb6HSADAK$u(vCudPu(Imbze!W{P-5+~4E%eh)m~sLChrPil*O z=*8Gc=i3hXY4H1cGEo6byb6mz)D{!kJvc(W?Nsj5eC!@9Rod`#H^aZaRPDYzPQk>D zx!LeX!nGmxbGIdYA-}S+nmIn`mafXUZeROz;+?sj_Wka?cWlwkCCYpKo+Fv2HFzvr z!Cn&e_df-QgecuV_Pkq$sDoqbr3DJVx!UTIH8Cd=UEFU-p$Jc-w$+B$a=pfzu|MeU zY(u@QE#~Yz9{DKl-#|I#`o5s^1(^2T4+Pp;osCmS-Z%3TMKAjE$zgbd4QM|=oQ5Rp z{7buu5RE#-lRCBuli15+C?uS``HBJE?+&*H5BbRfO95wvX1+1L?$vVx@>HAXwWomW8}M^*H8b(=cAyrXMg zu2*|;0AzOT^E1G!uU~=%3W>3rBO`1B^i~;Z3NbRiphcyhgRQy zvLQh_C89`hYdZC!TCQxTkeJhO8V(Is1Z_-mh}IPPd=;BFL3Y@sc;z2YvVjCwQuC1CA70FX@$bpo?Bu|)(jIYbJ!@ZCGd=l z=xK0qs1$)DPG1j_d6qCxS#j>-cStchi#JSu4zocUW7^-e zQ@EtHemT0>F+HMa&>lx5TIK#L%p&P*TYta;a$@(T8Z1WAmK)+PJ12GErh~Y9m#KZ& z{tKhoTdi;aR!SM^q_F3nMVRZI`WF8*^G8F{i~2%ZO}f>95Lru`%@^(gHeCv&X7Bk% z9XYHWHee9k=u}HcC$f(Wtqr&-=~JD7 zv0|Sq>;Jz>xz3;{mu4*}5+vvBA|QEL!UnQLSqV$d5?pd-K_urWQ4j%%0(!_fD`^Rm zK|r!BNz@eqiIOk+Ro(N6_tw32zNvcubj{4$JvB2=_tRZd+82?DdRDBXz0W@EK7l+acRQ`m)*GlVKZKoIR*St<_gAssk-&vCSY|vxOd`ns@uc< zc*y$#^G;zM!AUn_4oD$AhJ;@{L}Fm(ItE{}${{3|^5A5D`fY33@>W@Am9%4HSeRH<)kn6^Zw#A2PLb`vXBnKk zpVVGPNbIr_MFL@+T%N;4BD`$OaLh8`d8cu*zZ7~Q`W>vR@toIYX&@2bVvfUSx(t{# zC(lk#ah+miiN4!_T&52YRbj2b7{yJAVP|xwOo?U|U|cP$uqW>_h7hcjF@-S*G7}GD zm*DZ?NHOonsM3jqC-E#}6tlAJn#-~)stns>D&-J%Ee<`%u6FR+ipn4>0JpXI@$1!_S4#BohXyOL;2aEyTyUTZF$P`JR1@ zJCSgOeF7=}=Hfey7my*10~a ziBOVQt&xnxqZ0h9Zwv8P;*0+}kHFLX#_JeZ?l5BeR=PYNp-(@;&3R7$!;dr16Uig5&>_mI~t72E9(O__#7 z#NfC+{(OI!GqdfK;0cbPoV&cY<&&GfK1pCTYiGT|_v3Fy!6&tYdI?7-A;AOBS*#^e?@->UfX2L^pt>;-fX%+Rm`@G)x#_qzHQ=@fXv?F4OoFSt)%L1K<9*PRdgH=3R5d8|!S9j6JH&g8-kIPH^IFhlP-g^a() zd}TWK^>D;aN02^=^ay$ba@#R>gO$q*XSa6R(Tay8Cpg5%a)uVQyfa4W7GvMd)EQY@ z_Qj<4gbsR(MKYK8Y>31fp)5=J%CF+Qdvs0VNf0SyXkr#Pj*}upSr?GT=hJkY;Jes^ zSuHlGfY9uQ-6!wced#>WeEnlbW~9)rqSV?}LCDQOW1u@doXlz=NJBc&PX4S+-%4#5 z`f%aQtm<{?(NXYx?IUjE`(VBwk+`u!8WrZAa&FHP*ky7y_09vOVO4v7Iw8WBo|pPh zoREJJ8vn!z5e59@NwfsvV8Aby<2Qohf@l1H5EP;!LjUH3)EUm9E(i+$DgAX7o7oFC zq5W7@B1`!h(uQ)XZo=oNSNVdX;Gj+{T{+Xl97W@Ku%*7Tb58a=F~5P%4((o8La^rq zpSsN$1a$(U-nh{#vGLbT=>s!M-i_oV^0#UU=i6__wC~I@wRHA$0$UiwNUsIB{#bC2 zr$qa4-u=2#KG(obO2!V^4;{sWkPfl#s3@qeQw(b2?(?H9MGff_!^;m%%|MGl{z>+w ztt#?hF;ntbmX5(??3{OsRaHP(iC>uqZEAFXoAHVCbAfF92eiH0zRT?{fJO=HEjD#j z<8otU!We7*z^zz{are@uhHLiI z7@#Nxaet^KZA?CLh;}#R$Y7o<@(I$imMB`O9us|VZReD1(JK(avzk3lI-N|0&^5JB z+2;!?q9EWg_t%%aDaNJ8)Wl(ZO*CtedxG?4ZuVOVN-%CUPMatWS%}Cvv2*S0XV^f2 z^wlz*>mT*7sx&VNGht>O%lrCCNP@}wAsvQ<-WyD{1yr}ZpBow^$2b?5_P-HjBeid7 zM7n-f8HB^m2BHN8CWe~H?R36z9J@DyT#YX7&@rc#Q(6m z7Mfysg`4Nzb*HC0yy}Eq&btK-b6uTa9@_y5Y^4=Zo{es1hI+H){L{YmYS4nJ_?3{? zdei}JUxpTXy6JXqr9D3=Vrpuga?AH|8jUll=<}NwL87z1t*L9u6Hre%B^jtZ%S-~H zKYkzyAE%JYe6^TtN1@!sP4$G&i|m^sr#8x`9RwjJqQ+-g*>*N6or;JXz;!esO)ZvW z^g2qrefW5yL4x95ILno4nMfxNBl&M(YQ7pG!^tvmy0~zeUR^CK>YiM}sFF8@>mK7W zPUaAfA3)QEfipf?$JU*vLxTsE)3*nXO=t_Au^`hD7~m?q;u@H*dCpGoT7o>GHEn#) z`NEaQ`geD|H&O%cn|-RW8tJMEZxpyH-lyx4+-*momSzva&^_n)s~hOR#%U@$$!~2`PJi zrTj9ya|X1#@RbK)?FWt8zchH}k#?~d(K9F`ex zL@ve6XKZ%R%2)ESJCeMgyH^t=`JzS;*ko4|$@eiE_St$Ywb^b14~hAnw^A~z#q~N^ ze@lKWem3uFhYxIlilN$cKMlyVlW)>?j`DzUn6Gu|Z^Mf2R6PpoC|Q@=jeCBQu9>`| zS)<)+P!d+f+P1d1nNqd+ZDGlbc?}TQ!9P@=0=$>@QOiZ0;Z4v1{Mj_!v~g1vqH6Z! zaNH-0pY!CwV{zAW;x*1U+Q#G5h~isS0_5{uM`y$f`ui61s+%7t{SuXWaH;1MRJ>$X za2iwJMxF&;VLP)3`ybHfQu^^fL7#sy;Qs`DAb_8d_yzi0WCec((!ayc&x!x97#ITj zcX~WdXU;j}4!J+3BJHi-y}s&pU_i0u`z9UD=RFFVx_cI(Z}%Z4nO1Nt0&C_;fHU_n z6Ob5WF`O=DV{;G;Cmo_eA?~!x#@!N(zbjO5_`NAikn?z$)nJ}ewy=OphvN~IZpO6r z4Bib~4XwKK{6##r&vey?DwBIWJ4?}Sh+^2ajqa!NWJ1f!>A^;tC%O?rOk4^{)2$2N z3GrpZEQb{qedW0A5j+a-aDnAg*u zVa>^{o4r7B{m2)_NA*?Eef2i*nZJ}6R%oQGxn9?xF8SD7V@V(>G@tL~{7zj$%ceuE zp#)p+)E6S{d=c0SHKVxzFy=O4CrLCWaaDXkT1{IG=~16OgNj1RR;G6mYul*V&oj~* zRD)b!E(|%9D8#XS|J|COZYw6|_dz*w==``2qrHxZC0Ydz$zUBVY_s&qKw09bFIM z>0|cBLKd8C+fCq?tAyj^Ix0vC;O*9sEywxdbNo#C7x({Qk%TYhRsWMC^os!cM=TNy z@avt@i!&q!5c=f@{(gr3&d>huIVQ2+QcPv3L#R0z@!L%wBIbMBJzHCSxgG9>-WV`T}$kOpGy+$(@IO{4g0iV&$3oz zzc$vu;PS3EJmnpNGVZ7GL0XgthUoe93i9ftNlZoq(2K2fGXAEQ2nL~Hnly-u>CWgb zvC`I6*jRjJdc%-nNWs7UXpLWQ#8*a^!J1F`R5Oik@D>z)ZhYUq3y+&`S3*VSA;Mi# zw-zWaYhUqMA6WtIndb_C(PJLhUDqp#cr>}Sh0PzmQ*S(s+V1_*Im09QwB=KaBB?Ul zq+nz-GE7dV!tnGc!F6SjsWEjm`U^{K*bZ_XFZC(4guC{Nyiul-`iI~*)R{CRL4SRY zs}X9F>qX|;PtZ1G(^f*rZVulTj~?sj0~<9cXcHdBizp1pBxQgYm`&hgj`v86YqClg zRBawiO$$2*PwF^(wlko8OTWcx2M1WHAfxW5>geHGefc6THRn9_)MFN<>A=&7%giyV zMIzSC6eizfmg{+M+`d_5%5(M8H>IQybiR3Jzq>|3UZpd44sn9;TzV>J5p<$>60M*& zYLs3=q8XXma%Fc`OkZH!TddABe}X9^??o0LJ1)KrFuPEcjRAvsF20GSZAsA>_tX*U zc0f%pDr_8EMg=g6^pnOQiOtP$Ufto{r}|K+RjX6{WsY{1wT+Sl8%n|zxUyuTNZc>=Zy&%!fx51wi2Zh)Q-g%*UPKW#M zygfwUiajMlG4RB|6KUKbP?84kl==XA!~Xq~*;nuYk6(HEbwTOKmk|efr8I*~ezIDn zN`q$y?u|~Wa}9D>Nf`XJu)0@v4Dlmr!@{6Xiqe38u^e`32A%F3GPXUPZ==)W5P z0tSnK0oH)OWf#!eqgbmvFmR#ArbIJk-#OHn8?p7^0$7% z5HXR9IP*7|h%i+6vM!<^5z))Kh(d%e$^w2L3l@QjUIehe&3BQ?7r8ti1S$l+JQe~K z24A)V0);~Vl))gEZG*tTf9MQ>LBuZW3 N assert abs(ht.fabric_heat_loss_w_per_k - 285.9847) <= 1e-4 +def test_summary_001431_topfloor_extracts_main_property_age_band() -> None: + # Arrange — the gas-boiler-upgrade recommendation "after" Summary + # renders "3.0 Date Built:" glued to its "Main Property" row header + # on one layout line, so the FIRST standalone "Main Property" token + # is the §4 dimensions-table header (followed by "Floor"). The + # extractor must read the age band from the Date-Built block, not the + # first global "Main Property" match — the worksheet lodges age band + # C (1930-1949). + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_001431_TOPFLOOR_PDF) + + # Act + survey = ElmhurstSiteNotesExtractor(pages).extract() + + # Assert + assert survey.construction_age_band == "C 1930-1949" + + def test_summary_000474_mapper_produces_three_building_parts() -> None: # Arrange — cert U985-0001-000474 is a mid-terrace with 3 building # parts (Main + 2 extensions) per the hand-built worksheet fixture