From 716581b49a74e6edd6f6f0f412968e7a0e7fead3 Mon Sep 17 00:00:00 2001 From: Axel Palumbo Date: Fri, 15 May 2026 18:00:37 +0200 Subject: [PATCH] Add knowledge graph author disambiguation module --- .../README.md | 30 +++ .../data/sample-mentions.json | 56 ++++ .../docs/demo.mp4 | Bin 0 -> 38384 bytes .../docs/demo.svg | 20 ++ .../docs/requirement-map.md | 16 ++ .../package.json | 11 + .../scripts/demo.js | 25 ++ .../src/author-affiliation-disambiguation.js | 247 ++++++++++++++++++ .../author-affiliation-disambiguation.test.js | 42 +++ 9 files changed, 447 insertions(+) create mode 100644 knowledge-graph-author-affiliation-disambiguation/README.md create mode 100644 knowledge-graph-author-affiliation-disambiguation/data/sample-mentions.json create mode 100644 knowledge-graph-author-affiliation-disambiguation/docs/demo.mp4 create mode 100644 knowledge-graph-author-affiliation-disambiguation/docs/demo.svg create mode 100644 knowledge-graph-author-affiliation-disambiguation/docs/requirement-map.md create mode 100644 knowledge-graph-author-affiliation-disambiguation/package.json create mode 100644 knowledge-graph-author-affiliation-disambiguation/scripts/demo.js create mode 100644 knowledge-graph-author-affiliation-disambiguation/src/author-affiliation-disambiguation.js create mode 100644 knowledge-graph-author-affiliation-disambiguation/test/author-affiliation-disambiguation.test.js diff --git a/knowledge-graph-author-affiliation-disambiguation/README.md b/knowledge-graph-author-affiliation-disambiguation/README.md new file mode 100644 index 0000000..ff3ae7f --- /dev/null +++ b/knowledge-graph-author-affiliation-disambiguation/README.md @@ -0,0 +1,30 @@ +# Knowledge Graph Author Affiliation Disambiguation + +Self-contained module for issue `#17` Scientific Knowledge Graph Integration. + +It adds a deterministic trust layer for author and affiliation entities before they are used in entity pages, collaboration maps, semantic search, or AI recommendations. + +## What It Does + +- Normalizes author names, initials, ORCID values, email domains, affiliations, and scientific concepts. +- Merges author mentions when there is strong evidence from ORCID, affiliation, domain, and topic overlap. +- Sends homonyms and low-confidence merges to a curator queue instead of polluting the graph. +- Builds collaboration edges from shared document evidence. +- Produces recommendation guards so uncertain identities do not drive cross-lab suggestions. +- Exports schema.org-compatible creator metadata. + +## Demo + +```bash +npm run check +npm test +npm run demo +``` + +The sample dataset intentionally contains two different `Maya Chen` authors. The Stanford CRISPR author is merged across paper, dataset, and protocol mentions, while the MIT materials-science homonym is routed to curator review. + +## Why This Belongs in the Knowledge Graph + +The issue calls out authors, affiliations, entity pages, lab-to-lab collaboration maps, graph navigation, and personalized recommendations. Those features depend on author identity quality. A graph that merges homonyms or splits the same author across affiliations will produce misleading entity pages, false collaboration paths, and bad recommendation digests. + +This slice complements broad extractors and navigators by adding identity-quality controls before graph edges are trusted. diff --git a/knowledge-graph-author-affiliation-disambiguation/data/sample-mentions.json b/knowledge-graph-author-affiliation-disambiguation/data/sample-mentions.json new file mode 100644 index 0000000..2d79660 --- /dev/null +++ b/knowledge-graph-author-affiliation-disambiguation/data/sample-mentions.json @@ -0,0 +1,56 @@ +[ + { + "mentionId": "m1", + "documentId": "paper-crispr-01", + "name": "Dr. Maya A. Chen", + "orcid": "0000-0002-1825-0097", + "email": "maya.chen@stanford.edu", + "affiliation": "Stanford University, Department of Bioengineering", + "concepts": ["CRISPR", "single-cell RNA-seq"], + "doi": "10.5555/crispr.01" + }, + { + "mentionId": "m2", + "documentId": "dataset-crispr-01", + "name": "Maya Chen", + "email": "mchen@stanford.edu", + "affiliation": "Dept. of Bioengineering, Stanford Univ.", + "concepts": ["CRISPR", "perturb-seq"], + "doi": "10.5555/data.01" + }, + { + "mentionId": "m3", + "documentId": "protocol-crispr-02", + "name": "M. Chen", + "affiliation": "Stanford Bioengineering", + "concepts": ["CRISPR", "protocol"], + "doi": "10.5555/protocol.02" + }, + { + "mentionId": "m4", + "documentId": "paper-materials-07", + "name": "Maya Chen", + "email": "maya.chen@mit.edu", + "affiliation": "MIT Materials Science and Engineering", + "concepts": ["perovskite", "thin films"], + "doi": "10.5555/materials.07" + }, + { + "mentionId": "m5", + "documentId": "paper-crispr-01", + "name": "Luis Ortega", + "orcid": "0000-0003-1111-2222", + "email": "lortega@ucsf.edu", + "affiliation": "UCSF Computational Biology", + "concepts": ["single-cell RNA-seq", "trajectory inference"], + "doi": "10.5555/crispr.01" + }, + { + "mentionId": "m6", + "documentId": "dataset-crispr-01", + "name": "L. Ortega", + "affiliation": "University of California San Francisco", + "concepts": ["trajectory inference", "perturb-seq"], + "doi": "10.5555/data.01" + } +] diff --git a/knowledge-graph-author-affiliation-disambiguation/docs/demo.mp4 b/knowledge-graph-author-affiliation-disambiguation/docs/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..be954aa9802db6cd7e4b8acd604cd12e515b0089 GIT binary patch literal 38384 zcmX_m19T=$&~CJ`ZQJ%6+vdi$ZJQe#8{4*Rb7R}Q`Tl$Vd(O;sb=6bVh3T2;ISm8^ zL}=>lVQ=ANX9ENT0`y<|b21ya8Zp_}voZkz0YRENnwS6qCA!)e8#w=9s=7- zz?R<5(Tw50QRvN`ZLEJVcJ|H|cD7F3ghmF221WoTLPrx*01Kh9iJ`TfkrjZ6n~|H5 z(7@Kf+QZ2N!067x&FId=#6oCe0x&mmCvZ5cdd456p9`Ul zg}aHd-hUREel+wP4Q$O!08H$JM&^!oHU@e>s!W8=jwaUD7EV7Dr#q*yk@F8>+q!{1{>; zw6?ReGBE$?>HRO0nb67F!sy4D|79=|+B*KvAVwB82G0LEVqxoS;%IH~1Nz|`TDv$J zc<33~+1ML6|MZQ1a^&o2U}5`X;s@wx@E^w1(ZI&U=_gx;diEYaw1qL?r&xLh#s>EP zg<+^?Xkp;=-yjx_CjS$dn~8;)xwGMq%+B7#R?p1N{zv+Mnf5=bRwf=lwgJp+jQ@|+ zv$3%K0XrF)*qRu*I0IN2|MSw(;J=hQnmC#NXgeC|{r_nHlRFv#j2uk~Z47_%`d?H( z9RM>6JtLvRf3W}<>Dhls`~U3zPix=~VCVW_I60fx16Tp@W+;c!%zMH zt0s^?5Ks)6Nq8_2#`o7%VD~Q$;}3*(>NYySOl&EGEZnV*-f5T69#a^;v^@|I=>Hys zyFEn$Hw6@;s=fcQFfeysHuS9iZ301!rK_^96B4v8zr1EWC&g-^MRosbvvNrdVZ;t zFyef+L3{4HjA9YWfBcWw$OU{tdozG43Zx+Khyc#A(+~m+ zoFs*q5%i{u_noQHWTquc!NZc(50HQ$k%|;k@g+S|2uP|~LB(sho>wJ_5%29UB6m>N zJ($ut0KY-(+Q15p749;_Oc2O~+Ly8ln^&$u>MUfa;|P1G1+cJ!h`!P7U&T36fPs-_>o0FP-LsSJq1~nphT*We)JVIQZ&O5 zsA_5o@$S^0i%3BUS#y#Q?cZ?$Vk>7IZP-7N5pSZNW*iK{r^nY)Oq5`1r*(9OlB8tP(y+L8urq;mF&ZKdt;# zY53CUeN{S7!~l}`PIt{7XkVdBz0lKIPfk0^3&%f>VfH_WkIA?CuS1`-1_@fSEmylJY7#BOSko4BM-Q34o)9cdJC8`J|#w_Co= z!=1DP=BkOaBqZbm4XW5dI$`l&5^Fcc_gwyVR&T4YbrB2Ag?uNDMDVjJsMhj2!h4`i zmZ08D46_*f?LooZ{1CMnT%A;#{WiCHNU<9EeN5vn`!h28`qSkSN)?2d)qN?*w#JF{ zrr?Mw8P_y8v$HeZIuO;FI0%Rr&Q-$OK2l9fpsD`rnfZN1&hDj?x5*Vuo*A&Km#>>& z9mUYNS+T=9=Cy8eGLl*N=sIcN)We2F|Ex}5f8gdqT*(2CJ4#`Xf1+wK;OM>Z@~PUz zDLs9U-S!rFt|ZkhO|80k+Gop zJ+y=9#%u&e;9U(TR6}6OTwOOH;(J5G_`66JNF+l4>43oZ<0IHzdhoWNz8W0bB~Ox_ z2*Kd4cfLkuG(v2yF^n>d`fmEDbQRc!fh_z$2xW~tBKDb;*p@W#cu;h|K1&_)&OaLE z#RjlH&Xfp6PRQ1I(kLYmSPZYY)aCjK7zP8tMkgrR$c*U*@x$cfd&=TrF{~#l%SAya zXvav6n^JEQ-N+7P7r-!Ev4?<5h zork$?=t-GC*a+&=A<0~3Z(Dd02a%jmU5{rmGr_-SueQzN4jWS_qpx)Al-qDeOg4z| z@s%8FwiVT_`S;iF_gH*z+pv8cq7THO69gvrrxW*Peo_`)`3SS)V53|&mgwUjml|ws5B~&~e zDEE#N3tS94_~-j6XAfQ#UouaWNK7Fu;}P|uDrWZqfx-k#ZjPRr({84w3yQ9`udFkP zK$Z=-U$G^NyA!{|&3E{N+7*F@wf_2?@=bKoXCK08AHaDem@wQ!AlZ`jTEMbi>NA;XI^%&NGR? zpSMjx2tEleb}?NHri8fI#(Un_D%kRL-Fq1d%B-4xLw({D>E04${rfI#^>i+%$`TtV zZXRIFkZbMg`g#N|m)|{~Em5{Wz4OO*s>{#)S3RVoEw@AQfxkj2!zTiSS|V;(fTq}a zIYMBImDM%;kdzL1cT(T-Dk8$tMR=JyriJa|f-CAfJ z;$f)KSffJWKWW1h((V@(i)!PyYV3HdLf6bZZ|w)zU7ZY+%Tx=v526HA_UFC%dHcRo zVviVN5QhfzBSAs3&{xU-rnNOO)>_Afw1TSd>xo~38h>$A5JdIj2$lt$J7#}>;N7?u zvz2#AkE9iDXf zxtQiMWT=T~oTYzCJH!>-CM`hhT z5`;kEb}$dAa`zCEOlvsgp7x}9{GAnNv4tR%fWt6nDz)(B=r@e`rNQ(f`%)iQTq;UT zSd(vJJg>0eoS&{}Pf4eRSLYFFI@){8aN<2JZx)1v6S9DTA4X^MO<(kW{YIV1ZLUaI z_*~w%lPPHcBQ}kznTc2Lq@I%NQ`~`3Wm~VpR@8f}G!hnCay5#0_$!u4*|JYuA>agQ zB_{q=D)K5c!HpHLkA)g09Z_2NvOMMj9oOVdiW+H+2c?r?w*s^gZ>LqSosTmH{!0%C zH>3tomfn`K0}2%inWi7Du!7@ z-TVFy!~zy9$ilj$tjwHw>_BJFml| zl9O_@y(pt76of*raU$72x%CKi*Xie+NI7dvKA49RME$T@Q#ivOnot2%CN}KpjNeE9 zWblK7RSfcHrnuFQ@veIN90o2hdbQBqy804#`?*PDTUsis0&8=^>R+c**d4*Z?+rmW z6WDuqV$sd07lm}7c99g!C$&e;iR2A{GwZI_1yc=hg*T-=ER03JhHsBkIAccHJZ~&~ zq8M@1jJ&!h!Xbzl(q}%yY+mANY|iyzyC)mUm4L6FM`%ZvK`rkQQCJXvaqjCkBNLmExnso zfcvlaDmcKTdn=QeYb#nDWfjK=`~4ZqzII*|$vV}XcjpFFCf1!{19QMfs`Tiw(8ZnLGvBS<^ zO>;bu%k94C@{+ZQ`h@PE(86BhkOR$rO97Vq%CP3yz;mletCVmT5mc5rS=ZAq2fR|i zuG*21O+&UY>7U|{UeDmCi_OvMTLaELQ2nP?!g)8f^1~3^{)C$)2yp z^75ec^Q4V|*lbD~0k!|A-(`38q9u6J`p!ltXw$CeVx0W%I_{xyOH`Eb|q|eZq<~A`F_c<>!bGW7nyGqp;L(@Y7Cvi4SY9{YQ`tEBSI;tz3O(js6jrOGM7ZaO>=uu^64 zb#}p4xy#Alla?3Nr-Y$21lK2Aa?fXe`V%T43yCYYEoJ(d-A!A(k3C?nMZ&V6t+-_@ z^5_(WhaRtnQNniWp>W5HgLb8I{i_%I4qQxKJ|3Ouz#-rEPxc`w13!=ln|^%1UIt>e zA}Q=f>3>|;SbsZtfi#Sw%dNUOOgzqH6c}-d2>+!QZN7NxJ;Up|5lKRFi75$pBXHN^ z<&hL#fi&+0u`BZAJs`y6XgyHk(ulr1a5{-SPq8am`n|JCV=9W#)MO~a8k(W8{C7RS zDWddQ2(^rLX{h)BKiDb|S%g;D@0%=Ng`NRx=`oBNetg`vuCNk&55wK#LpO#T$4ALJ zOMu*l^u_lW7x7^U4q2CvbN9RgP5+B)&Nd^kIbo$SLXrS6UHf7G`^rR=o+&kTT{-_W zjIxZ^GD&L{^gCAOMSC(xmyM00AsB?w`rcyR@b0`H1QcPNOlf>FMLSy2_umrYP^TDx z94B;>`pl1ODabHy5Qmm3j`@pEg3i|q3jq#xGp<<}d`D4o5l?{QHh@EN<2w30FH@rz6fP5Q+4;tQ2y=k`V!p*^wKB?ed09IZsdx#+F*-7zm4 z6+Q$9w|k3^+32cwKT*Cn0Gu=CoiU2_Lsv~Z;0mR6r!_=0Cq<{!nM2-~cXe@3BumUt zdqxWOY&v1-Hi7o|oo5{O+FzA^nUsfB;i{=Z*)_4YCnR~ez%oSCluQY4>*%Aebd;LA zVMK#K{WSI%EYe~Aj!+rAcc<9#jCj1vBZ_jc#kjSf|njK}%bisNQ!ky+A;QR(Xg+4K+9EFY@z@pZ&S!H@DZ6js@bt?WPBj z-a?>XvFKC!jVbtIU}-V4jq@0FPAi|s`KG|QnZ+jNsSS%A?;}ub3HEbv-Lf~fzRLhR zV*P8rsCxeMEnavv7BEd7I{rR0e)gZdZen+d>(hf#_viUc@OaXJ%q_AzN+h9*JTEjp zCTujd7V(o|=i`-i$N{)5KOZ-e2>6Zcn$Jw7-{@{dc8T>UZT`-O9_MD@4%D&-x1zpr zWqE=kp#IQM^iOkS6?rP@!;t1$Jf+S9DTc$F73~N_iBBXHBdHf(xnFGKs@7i_8>qF9 z&FO~{nn1MSblk^Wlt9~{14j-Mx_hpFq8LsPh^^xK3-0d#tt;A@BFa@t)gC<#%_0i{ zQLF*<{0*5s<j+*#q$}a^1I8Gpn>Qbp3lZx}Ohl7tu}4gUIH3 zApq=Fx#t`>JeHzmG@?(k5>QIaYw8Y(z#e_U`VU4`Vx-XR#(O=2E8>klLUO^?_VIE( z*I3GzI#ge@05!Dxc)NqwDp(LEE+0&a{_)D{#>DY~-#G!7rd&n{&u8#Q!T7@Y)r7zP ziCF$&(cPaJk6jUC&9X`c#Uh{KOObqd9RlzjP1QMbp9^O4O0|U)ZmP8B7)1Mg(WniE zc_T1Y?|JGmTp;D`j}Otmem9li9S|<-xY?vSp&enqy&GQsWok`WS@80n;TiRO@5q6m z%PxVgHt9kz$B};ehV!NYJD~Sc+VE-9WUI%9aGT1r!QTn%5W>B?drlqjA_ASkI^kX2 zFcx%=9gb#F9-{y@%e@wg0k7Vn-{oelGaeOq%yWH=8%c!ih+_Y0=eIb)o2iWZHm7m7 z;Ey;>=qN23H$&h$NlYO33L^Vea>V$p*MZ-%5Y1F_K933Ev~ZGtZYMz;QdZvJ!&-5q z&rx5&tdt+%S2~go(ef6v`jyMEq^o;5UpbmTz6X5;AQCRYMDx^-k?+-qh)9nA_}YE7EN6~g>T@$bTQ zmf5P$Ru+_R+avv1hmn|)7KLZxc0t6>9yd(KReU`QEyksKIy?K*@F?NL5hbNw(EJux z*`+CStJCp7Fyf)_YH1Xlo6lm13*w(vvyo1*!WhsI77DNa-qVW)x-;gh-wq_U+Pld##UvSTxo_q=Q|oysV40PNZwpFyAFl2R}KTMO}*a?$?X3RB{}H z^p(U#GMF|^D^E739oi?TU9B`s9gO+KvQ+`b%H^`-@~2fU zWJm8_pN=htLFv=eyvN5H{&i|YC--b%Z*V&oC3MFW_eLZH)FQwph}U|?$3Cm zvH>$2iIs6g&d4*#c79+zS<%To_%F>aVPERt6yP#Bb$og+`d6gdvK>u~w5RcX>`~FB z=Vstb#64dD`qI9w7F%y)l`rmqK-`Ck^`pE~k8CQ^Vg^6+UZA+Y_7vW298Ik{ak4GJ zaW41`ZJuv7?I_Q0Wd~;y+-p%xPpzBzTs`NPxMhi3RY;MLfOaMYl*o(kGgfU^15KdBOW+- zzHVOt7(PMS2&sxg01pqeKOv(zpwMyOkzipAi{8KwXh&qXb)9RPD01RW#_K3Nv&}Qr zOc)~R5!Ww>H%e?*J88SxMhm%S?%AjLAr;*NpOu_}w-ewmmz*M}$cXyxh{D}@XY5iv z?7ZF3oYQ!p(MmF>=EomCP`0u#IU|x&j6K123aFg81S0r51u=`Cc-;O}BW}e=wx95& zUGR^Gsxj)^YOnTnD?WZ?sPD1A5iIp!iey9q$i{9<;lE$NC|>5@p)p6)ZKZ~~-0;d{ z9pjTVqqo83_4fOKZ|m4(UdCq$c!)Seoptmi?|4WxCg8_syLO#pv56Z7%o!m8cNa%M zV%8U?J98R6F&ErcU*c)UfH>@LA4ZK$uj<2mHqlZtS;~bE_-hK!j2QyH@^>q9uMVCH zGY+*%rI7L`NMI%)Q(UxfUP1htPvma2X)Er!*+WXM*?IST*cz!L+w{hG`^hLfs9|Yi zGARtgVdR;8kK%+d{UdlufF6tKFYIXflZzI_JTDz_1fJ_|XAA;x#i=Ugmf><8i_=7M zha+9LJjmJdl?f4$0gG(NKE?qK%0P}3Mvgl3jmAhirt9tc`!my7DcREB?1MAdXY+3I z)lG$cxKB4{STb&jF$wMljY*Z(N9k|JlHqh+KBXeEhRz0N_@vlyKoQXcu_`&hecEfW z;if&T72+>&Haxj9GIVc+yB^(BP$7>PUF__=DkU zcOr=Zzw>SBI1}9WMD@g6`fSaLH%+8Z78~f5!5-tz@R)i}`1h@zU%Td{ZH?2{$Z03* z-omp>6ntuU0KL1H9&na@Y)0eAyTv`T*>8_S*<0$O@z}@Jj0Jo3@CY3{??b?glDX{; zg9Qrf$%)w3SMy6*ifvXbXrnc^EB-+j{>Qj~RA4MSshWbqh^V<1DuiEq8s0CzFD&2M zlxuvxh<=a+<+GVHSHHJ=dh4tmqpm2{kavWzPMDwRK-I5FQLGaIo9^w5dx(ESpKUvR zbZ!Ob?nm)gM=GqgaYP105E3>iT>sx!IRBIa18mGPW*;IyusMbA--YCELxeqVKbHzI z%S(+^OH|YQy)4N!aKLJpg)96q7F4w?Sv?lzjGlaO<&C*NLx1~Pgzc~X?>-A5RvN-C zXvtIbWK!V@d&9@ZU|uP~TGGrtIdE34TKh)@SZIz?_Yl;l&N`ck(GI2xdV(wy*w!r7 zu;%fsM^r6j)(nf%klUIV2SsDLf8LGAV;WhPo60!{h!hAQ+d=v>q z@1d)SaT%v0mWt856X2;aHYx?c0O6G~sMAS*=pdv9Ey~Gf7-K+Rb@a4%FDa_6q;08` zg;nnpEtvjyOCS+H^TTPsLU{oYW|iXD`VQV*1@)>=-kDQNj+|g$p%!oc$v9Z2^Y5X} zWi)Y0K?GT;8Ij1C%kjey)9|AsZ0FxXuhfxbyEfiLGW-2R>&C7*;_xlDOeXwEam1aB zyI_0{vIJwQ7DT=oi$!<3NFwz}*K6*#)P}9DKH^JZp&>WRi<;|SD(bG(*(}&Swan33 zqN*$&Soz>x>*=o<27+jh84`}nLNg0wv>qNJH00sgPnMs#s|cxCxGCsy6q#6~a?zhSPE@dgA%FKdCv*3p* z?NMGQrXjJO&-mk?NknziDoK&H<`u6UnE;-rg#F+oCWsC@TIa$tkFiT`)b(B9M!ck7 z>Z;hQP|pz*ucjHNm-&AqNw;}?-b`94OKy_L;l~sU{{2wimC6|mnBG5TldUaq`^Q9D zf`SoodzoqJ_5>5DN&0(sFd1w_xC3K;V)4caR|BDqm>d%HR6jqFo65{!0NN48Ndu-! zUCR!n*-e0ra259Ta(#K$r8a6yVw9$~XX4B5<+xtnw*`QBS*DJHc#i+Ovhvb{cm9&y z*4)O&0Q4C zvR-&n|MlB;(K4p>3dJjd&Jep?4){yzeMh+kQL43%x|mbQ(K^qu>H{Z849Pe>U}L$8 z=()0zsL89x()Tjm{IAya@EtHCUi;b#M;v+=WnL3?M|pw7QHgW-m1S;l;8k7>{wKR^ zss$1%)>IUdfsXSk9e?ivc)ucL-km3khyIfNtc&+_B-f0*xK#bv~~6< z%07-%IpAvnCjt}15mB=Wgf#z~hkqntg`l%11Pwp3TMXvif)YI^?id=-9jx?Ar8=_H z&D_T;!C4N}OH}rqyK0{K2*Gk;``ZvDAn+a&GBT4VkhDH>+??*-rmvfCcI}Ku2Vd&@ z)R{|Bp6R*Aw;+`U0FNx4GIvww zoLz+X7K)QjLg9@Jw_PAU_ayXm`+A8ksPynefT6~k@OL762C=L)7~vAkb5QE(NjB<$ zLOe?;j3H>A#wu5mLs0Xcumw^HV^lVSKSbBS8+jbeqx*qo=u$5r`wqpWw90Bly(U4X z>Q9EAatXu2&FMSH6k$1p+t&vH;`_B2`*Ba=zdWFzG_=wSX&8r-0~H)3p4#7ckLScW zG!9S!p^oo8vz$%>R$daCIW_;nqv^`epYjPVJG)j1Q}@FB)N|kCJWjr%nqr+#9a)SWjtaEsr^L<4Kbm zowSdKx-HyD{(~ybde8p;?X($%a+p5TXq{RpOPy$`zbWPCk%V-(F^$5&eoHm)nNs$Vy731q;vzLUo-Mop_v=-zACa&dWJ__K=nIgQh~mm)uriV@m% z7-=(7I+2x?NtRincfq=)hLzQp1t0VCNs+&>JpS3!DambhIvTh$VSy}b7B6XkaH*i} z;&Wx!n!w2*gjKt_rLaG6#v_S}riQ}k zI9fwEDrfB`hvGKo>`^8j!Pml=4I`=Lu1_f@8qAaaYPUlXB(y|+i&2W=yWBr zQnNoMhCJjdGzMZY5n z#9guLp0bLFEN>;hFWKFqzd1@SEm`F9$)|)08X&;e@(5QX2H-74o!8|`yd?P44;LP+69DdEy^ zM|ayX%t!p|ydYmUmoj;eM$hx%DmSFIj3ic!SE73ISrapVLqH4Jd56T$S~+KdKy`){ zuEyrhrB1Z0b+=Bxbx^HmJ)5kkjK-V&y_0&fNPC=fETkpKFa=VT z{2d@!o9;0i2rlD83ZIF#GT;G~l24^`!&$u1iW_T2re+j*V`rA34UAS00$OOWJ!Lpq zm1UK7$YW==a;NQj&-?%C)zVtm9B2x%V9JwX6BYC5efzG4qd6I~`5L9gmPUer8zp)k zpc!*{unhB=y=BN(i9w6`t1`+VZ1<=_t9GbJ=d=DS!)9QRthHd;lv!4PY!MX7NtkD_ zVK?Gix9+{@~(n-nOjUg!_r`Svc3p3I1n3D!!*NDQ#os)lVO+j^f@XeyHGzQ zL!LHl2JP(EtyPf9cfR)d#@Kn?YT4{N<{jf6@d>t`ElOf^dm+Om0Um$PcAi-yfHEVg zhZ3gZU+)b9(uZoXvQU2x-!w{YS*epe99fJ}?)h>ye!1gn(P}Spk%q6iX75fHHjISp z6t}FTLIqP5JHPNAZb=UcILR;?-^N-v;}BegT;l#fRBF&8prXwp<@d75ok4&>ZuOkhh3(^Sa*{enNdt_qOy;a|Z`z;^H^3MBjxPe)j;wXJwN zg^;Xqzb8zl8QwZ6D9&GBY!963(iv*|(`cDh;^O$YfC4%7VcwIMfUr_QA?RQ@1~X*w zPy5$6NJ;0bK7=@HHjxY@5w)=Ek77H#)XE7$ zeee0YAXNBh*HU1m4jUVXM(=@2+}VH~6pFc+Y4xKL;0Av$TZ+zw}#3rV2AGV>Px{m{}yrxc1u?{03g2Y6xE=|P9ntGWIe zFR9q$?*ZwU(0qX`W<}&1DnSk{DS?ZPt_#Ig&zin6VdgY->_qlb?OtH}>M*^US#)D} zo-4BF5#3AhTRZg&0pK2CC)wpg)YqI{6VapAAIWTE)pA}(B46eHa^^Dc?8}+8F-;-p zC_ZGC**~U7aI^mGh>bE#^LneMmOZMNa%`*CQHMs6G6Fo01!s3iaBL@ zY|Ynj9#&Yj1Qg`Q!(4Mx$aGNjE+N14mkPjdueKTt5jy1=!+Pk|>9i$!+Fr`pM#^$x zd%*OuP+kIr;6lKtDB+M`&J-kFTBx{Lx-m|}vTAKqinlSH7kuzJdsYt{x@&EK1^zEP zMWM)kkMR&^O(>l#ugJhV?p=xq=zGwSc^#BV(MuEv1kgWuelM1Fd|0h?;ERjqT#)IQLoXPCzvHC-1ECkVmol9d+>tU?NEl`B^Z(84K+)CsK5H zh!)cUI?|9%oPgTak_!uKO`{|}xu=zS53~$U^Q@_b{=N`5Qo{o>o7;sV>`>$q zVpQqGq%GcJT4jZNrCy1G4rp=uvPUe~;m!a`Cir5z1Q zKsl26)tixaHoLw?g=g$wzk29^EzlOcv%+&@w^tLTJRattSagqP$>T6e5mOm1Dy0ZVd<3pJuOV6_7IlwYgFjKs zoe_*x?NT$>0%M^YCzON7!vThP#XRazuh&J)REo%~pc9lt&6BX1N5I23JZ>QrlccXh z8OG;}ZbrL;*kh^hOC}wP8g!1BsSmVu+}Y=S?f#HajPFr829vHgE@b%N$UB4d)uv4k z`uK}tLD0c;54~lXp-4KfC9_aRhu7UNR9COB5NLbLlt4tObbAp|mM-N<@{Vo+Q5w0& z-Xk2s3M|!dQvC2*F!SlEN`?Hv-PaP7p>L?udFAs83w$cw8=6eu5h08@DF=hlH{*{i zeK&qnh*H{9b)BWTPCtx7uQYu8dZ$i^Tu=kb)Pd*BvOYm6!X~S-_Q67S@m_7CXlT!u zo@$ENoAdiH-wgPyt+VsFsm6s6m`A^WLX@7S4q7yFS4%WWRN_q>h!bc;RO%hj*q6Geo=Ywd#8Ml%9W-Yf|wNt_dTy7@YB-fxgp}S=_}27LJBwD zrA4?{#@Ydwf~lLNAd`clXFuOJIUMx)JD1GDcvtuwY1aRv3sRNQM%)HxTh|{5_U(Po zv`^Xjt75~76Q;8Bgj{OMJBKR62m^d(^(41At#oDf)_28fOcD!7PPnj%`o=OLo0(#v zv7ci@sN$Z&5xq2FV2-9fju-qg3A|C0qVe*acIbrkwA02CJ8;4Hc^fpH<8cZZYQfl$@}qDBviS9)7W*EPu_*|H}qagnc@>7=p5A$2zab z2Y}9UQIvr|@xX0wE{Nx2OMwt+t0FMIm|)K%gg|5M0iQtcRY`YdxofghI>rYo34XOI zC*%jwPRq1SDRJja@z$8szxNDe$~|@r+-52HtJXz9FMTYb-TRJI$Y27cBtMA^ToXoj zp=tE3{Cg;=&wOHTf6OtbF{tBUZm}K92#M5n-W`-#`2_eP9leKwA>^`<3xj^HMnue$ ztVfTf*y~AUqU}IiT4vA_A_&#Y?1`AzDv%Sc+!S_nEJEe1mz8yE#5RNAdid-2q$c>z zce}Key^bGqd=Kj&=3z1hRKNt=?o*|v!UWbANf{c$9+5v=&FH4gLoZ+1JiSn?v0QX3knjC1m%gG=5kirwPZ}Qahg%N}nO`k{@lLpB2@VMk!DY6$0nH1vSrXxld13d#0YY4pl41 zoNa_<7UYRW$t6~eJK(tavM*z0^K7YK{nf8vN#$Z$@#mZ{q`?g=5zy&_E;4rP`H6+a z+XKxO*Vb7M>+?bBb^2H>o7$P`rkP2w4zVYgnI5QCQ8pXDm6ZmWHFQncq>AbTxw<8s zAX@GJfx1c8BbnBGUn-%5^vRs?t+%)5qhj1L)LYo=+#7)Z4&?)FqV2!URw`JWM!9l| z8AstRpKX-3ytUc_k7r= z=w$ALw4d@=bn3LApb22>(4c;QGdcaUOEJlm5utviNl3GCZDv_vuvP26Q`7sLSlMB6 zv$gB5<{JCKqB6Wj=K&3!l1sBIqKp`Oe&hOmnYzwsHB^qFPxruSXKhd8#oJckIY@K>_Bu;e2z$ZY@ot$&8Ex7jOp1h9c}r+I9XmCvC`_sg zr`;QC_Cz%7m4_VZv*{xr)*P1=*~(sgW(0azY{z}B`l&-F^Qtp27yDaM)};9+5LYtj zG45wtwG}iQ6%-E<;)Q!b?I%i#D9vo#>~1=i03jZ&M{4ygLM|1`qqqD1F*lp-VNVEw zU}~eI4(m>bxw#%9S~%?{lwZE|qwE?u)mntRc-CYv-e&$egMc$r?XW4Q1q_ud`+0|U zCN~VnRM1`6@68Zr`g<$293z5V#eM>t{I}!n6%sjC*R;vv#Y)?=VzClpPqB z>KMybsDzz8eJGJmxqhKn3!c_th&2jR=>sKAmEc=Bdnh|~SA4N zOOBji*EJbC5Mi-zdL*3hj@cjNpY>|o2f8e*ZO$L+qOS$rr$?Np+UQ2HW<2cW$i^nDSdJQZ7&0)hLi=R#Q!K%+GnoSx>;z@3#%<)~G!5?n7< z>~6Z|xAaZ$-EYe6<6+io=&lWwqhXnyVxzze!fMxR9yA;uVZ_qG_yYZd-)|d^p$sql zt5&Okk`65;OGbZceah!IPX0L~>CYy2`x^JQyu^*1hxdg?c!1f`c{dg=@jgav=JGfE z_0MDQ<1=d0iUo+pdb8_zp!h`q{;ZTV^`u<6D!D7?iM*s4to(`>_>$$SFLb;fQ8`oA z7HhiX;=8E0w_zaGD-s{#3}pN#E9gY&#v+~6#0X!>H(}-pa}xD9zfexk#|@-Cg}e$( zD%ZhXS|m8mSTE?C^Cb~=iya09XWN(yI#9s;rmqnf<80u-+8`gDc8GW5l0{U1$mTDL ziW=)q?_y|3YO01*#I{Z{;rpY&b=cP!`MageH`H>^t>b}V|$ zosaow-8?`SPS6`ard~jD>m~+%}Oq-A;D9@xhf*D+i z%4(sk(}CPGNh3# zaY)Sysl?-a*vXb=vxIYSgd>=zEbAc*f``m=0HebnI4%UeRa5I{`Cn9#Nkm zIctNa08CAb4PJz%Q)r}o>8_=D6o;y91t)8w)l+$b@YXr@QlGeLfqd!Wz@Gm5W)2NjIdh}u~Vs!KpJCZPfX~eezrU?dyq0_BXky#mFWVQnGwaTIzEiH7_Jb zN%k@c>63JCq9XBkI4AGm?2JFL%Ok*G1wvHs2Uza95g@FygE~uKeqTtCgPRE2;C*44 z&I_?>&>tQp3F&XxCBdELfkAnLT7Ya{)<}r!Gl zLKHLLebldE{6{w#m^3R5mFa_z#`RIc`BI`3<)_1lF5J>cDBj_>U}f)xe) z4N1yhxkD(kXKz_?-klJF?)PJg{-G4)a}Px}xL+<)97JL0CdOSH#O+7Q+8AUAyPd4( z7k#rR6!TUKm`&(HD8Gus1v@LA0nNIU`K){*Gx-3$WyD!^fDaaN{5`~P5I=(Y2BRO! zrr_-CH0O3itlBtI)0yBvdPix!F$U?_I`0ms_Uo ziSzTq5|`ux4whn^7j5tY|0S zOzzE@rW^YHb6thj#~Y(#68fK%n6WM~auPPLO&cZ$vi3VKyPgbO)?j*o>laZN^Z}aHLUZch-$GY1XXRem{@W5p5@jvI^!@#Ye7pX75-- zvHaycxljK$aCH;o&~3}voSp4wliGF8L;E@hn8ZWFnG+)uAgj7A1Mlr zH@TAkgl<|9m?2UvEn(Sbw&3swMU$&M3b~QCZC8vNC!rqyLtK9(2Dotg=u4Q=^Yd3H zI3&Fo2FxxKZZQ%bv;=OQVcii%rc7>{M zDr%0gFup4wa@4UjE?d+2u!%qUyQ4n%j4fo{nQH<0k#>59oYjw?QGiIdsT$)1`+Xo6 z7WwW%-XNtku}SIgreGUCVGceLd3SZNdRY|ok7oTPY^0T3`ra*(2r~C9fn(I>D<25u zTN+w2e&s#x;pv*lM2nY5Z8RLCWkku}gs~ZNvWhR<>XX$KnqI-`n_`-;I!L&`=OIQj zsaXqUU|&OD_LN}4Kxb|F{{TQhzrRwCti2mb+GWyA^`TrawY<>C`DhM2fqxaCS7@pN zhOePN>=ji~?$Ab+;`y`-gk3dDcggS#&LSHpOf++Z|eW;XnvxU4E?zrHY| zGF&i{hLV4KvsSgBxDpP_V>D@On_A<6@vR1NAAfpY_kPv8Bz40_tY#_OlYy6k?ufD2 z)Q_r|N0mz^ng^fAsFDvPLB|rfjO3aH@}B7uGZ4A}ZOUAIwmSv~6!-isg4vl>{#oV) z`IzRKe#0Sfme|h2$m4GOh_*BkLRzTQDhsB13fFlcR)398LGiYcbR*3Zx1IwuG0rrp zqdUmfVO_ll-ma_7w(g(+&U{ExM{5Y)s!=#dbMP@{4MTm|O-MAu<)tkh#w z*ThGnnNcd8KhT*iD|L)FU1ru52vHEPN6#*j1T8HyWU|d}@G-9Lyb+CqEaOmUBv!7cts)7tOno%4V3Q|oc{0C)U4aPe6|_VCmMhL7a4|KVUv zP;>Nvjmw!|`BypTBmDWEkwatnOzwQbJe-d+ zjO_kN>AfU%Zc8d`F@yQS|)jN!a9eE02VQ&J4Y3saG7^^n*;sCS+#O5$+kG zTE@mE*Lp*#7?q2ehktZ1x_a)cG;H|XMSV3?fXJ?-fKG~=Erpme>XIisK)XWjOC$>U zL}QXQf}-zo(V>IHLWlAct2ZjM3xfQ3yvXhxfMKCIOlGNqX7P^_>eTwO9@97cF4K^F zGX{7g#qRni8Ko@FZ`IR!98Z)`y%qyJr~q#f6ns2p7U zeL5hhYJE7w6;Gi8FeX-QRmhtT88g86weV}FFk;jVBW(qu9i*oaR3H-EFw{~KW-M?k0F6(TH zNhy#5?46%P=X9TJ1<$ch<26B8WQW3|YyP1+pueRFlzQLI)h92_@*~~~q^Ud+Jx#AJ zijsjnGpSI_EPT$xG!J?4&)39~FRA&7Nn+Vuh$q?(&+0?9QbE3lxKJ*huW~3b0kk=1 zDqs~~WP&?bZ8D1{Zuz3tAux6d>|z1eMh#8#dYy5duOOo9s_F2Es$%@JxRlAa_POMo zrbul_vm&b@sQc}754+9vOGv#pj3Calv*ivYUb zOxZVn&}+mVsYkG}d=IaG@Nqm7PRX14A3&TgOU>o|XV5uS%%lxg-JiY=`VyU&!z}8@ zl8v0M?+M@1>1jYLuC!%hsT%6W^y_TGJ#l~AaV)MS7fPP2`r|NV=kY#QJ$L)N?6j#} zZdNcf{X7kL=KE8&dL07T&z=)U+EuMiJ<4Eb)6Ur#^_{RD|5Ht~Vp(Zfbv4uO^g zC}tdPzjjXD4W$Fzx(?b}>h&^=yWZ-HIp2X2{NUxvx(+c4;jMd}X$vH;@hi_n001K$ zjAK}xv+i<2HfF6*C<ruWU`)x<@IkSD1LNUc#yfm zqvl<+EDC6bu&@trg;v5qJpKrp1Um>3yN>EGC^;{QvAQAOH9gnC2fYdVx!E285$^9e ztIL_S>&@tvq8OhG&xc`W0|83bR|zM6a+CXAngTV5A1En;L&qAdxwCX>7L{r7>k!|Z zk;gOTG$S5DH8DN-Qsi^vS)=&fZPNpZ5SD#-t27ho-dN|-&$aiFS8b7GSzvfO_EjjE zR^t3s2{@)8BnkxT?tl!-3Xm$U4#@lgpT=&jmoN2#*z87tYdca@7_~qxKmVS)M2oGC zGw`xxqZG80oX$@32rrxKmb_#XR$)<3cyILr4~)n?QF#~tSvqJW2bFm z)66exFLcQ{#ed+c6D{6ScbXb3sU(7)E3KsgB)-x$;1!m^Bb4$2E zUwS;PkEarqDMBIMfW4N!xzTsMT=zydv8UeGLZM#ZbzwUp#1^o{#cxp#Zf>Qrxib$4 z609FEl_k=X>DiG+px9$;^n$jeLn~hTZ;&-?gz9y>4if=KXD$^Q6>2OH z+a=P1V&)!T|7(6}+Ooma3Wx0?B>O{?0qXHNt_O-_pw4?xEq{ndWXUG6Z5h&oEW z?WWADV)KT^sz?0AxNapgmr6uIo4xjiFFXgpq}(XHe^D`nYVTy&pY%l(ZHUTBLCZIl z!D(ITGir^xR>|p^k4hW$T%bNL_{{za6k|6kD(FsrIxON8)ht@uIRo=FecS|{L|ndR zq}!F5g(tU?H>TS9ObWV86gOE_WMo*YZh-#osdf7nIm-cS<^7Gp(4^1h$6J|{oW>p{ zX(w=PHqF*~6D%APyjXa`ohKWsQ3Ib)t--T$D{RWxkBgZPGSh|VFTcRpH&MET;*<>l zs8(ZyDY1JucHH*^;x}VY>RD-0_P^h>Zyk$#lSbUojPDuD)!Z{}6+(!2gnB6X|@k#2t?>p-1u;V~^j1+vefOWFsPyKS4Wg0N|_(E~W`BRti^c3&R zLY`>yfBQJj1&v+Vb_%K2^aT8Q*3FgMx>XgmuAStJ%yN8>5zI?n77>IdQ(SZ(?n4u! zr6Yt=F%=m&R2j4oaPX@&?}$Ls{-Z6H5omeVn}MJGDFDl?w7Cb~Z%hnE)a!%>39Ic7 zJLjd;Sr1W=|Wb zfxq-3Fs(=w6y%m%=3UX9x2|^^_dcE+Kqac43|Pz+zx%8`myIz7k?vyH>u8C|T_YS5 zl`+u+DYzI3sK%DySm*Rn6RrAkdM(p^!@7_UD&)~uJZfC&a&l>249A?J^ZV zPEaLUIZS{f`tIx4Yt7|zxk>i>p>SBwu6S)3$`Ei%(#P+_-5?uRNvSHaV%G!2D@0jS zZIKFN!8CialZPWG+~CJ(WO3yJKvr(2{_S=$0Y{VO#@3puA~xDluw^@7V_f{0WuCxc z5x_{ddd<5pxBcfu&}A6;bw(HU6nrj3*UiXSv0T~l-)eRIMwOd@HeTsCU3u3ql(-i5 zkw5?jy5}-)E9l2sbOtD{j7J>I^=(6=bYjZ^lJhEq1<-S$LJg;=9eL7&1_q8Pdn#Ow z8`dqJk_Q5+1+LnwIJ|jS-imfWkVKx_hUx!m#&fOccC&~JUxAQ-joTl1xdavRNDO0e zP=k>n8=cxW2F#T8NwksWs{M@FFr{wG2EX?oCmt*JWH+T^Zq2%3theU=$9!6dwezAU z{X}|6o2OY+Wk{}lgV2@M?$an-8`WS$%6U_HDA!3Aq_+5ll>bPT!yfD}B%MIP|J8ve zejH<+(d&JhC#8?_o!QN=!O#Duu5_V*=wStXv%eq0W!{U^OiSA}%jKjtdKH-uZ=9=z zA!CosRefy46BR8i&wPHa`S#dofP(8Rv)M&q%xr^C3qg;yR?tecq<*7F$)Dc-OCxb= zUl3_AJuV!TdF|>uLXb>^wJbnB02vy}?t_z4%)RyXu6nJ17V5yWmx0``KftbcDse=k zw--fGTWc5<4c+`H&;XTx%{NjVj-zpHh$lIzNA1FlWU|dw;0}4AAf-PWDYqb zA>pvsKv;+y%LQYT{S6sDmqH3`@@$Kqle8n5Cwcg`fBxnqv61D8(-X*D0rD(sl9vVw~ zMYn&qY`cj@EQo1sl{>PAvZSTdeEd}kg3zC|AZdVYCtFQSDV=(6EsAr>;2h`^ZFesT2YT{5^CC#prw;oFV33s@Na!5 zg))~adGFjmd7qJ7F#3GLt6z@1fhhR^+1vsuZ(qUtp58~cV1_-`SLcBM00RI6k9$J0 zp|aDp-Z0TE+WeS;=uQvJK&KA6&lUzue03?;-U;xV76d9SABw*7zg8b(qIAIkjl`_0 z`VQ9*?#*ITi!T2Nk%dT2TFzZ0`K@9T3>jS@FF*JoqmE2+l&-G(v>jb!zeVS=2hp1* z6N2i8e?YnzSZ|@(cQa(BK^Kaz{8Am7L4(@j7)n`MyKIgPrgmlbAT;B2z}YB%ca4p9 z{>#fY%W9);T#)+Sj7WH<<^wbQDWoF@w_K#V3Xhx}I*Oc{9_^n$WIj2My28^<4G<_O zm>7onxZfW3&4du{R=L8YBh5O?(pwY3nm6>PAC7qPNCsam&K}(@S?pg`NnISx3hT8* z7sGd`1t&A35Ie%?g@MU%*hwE)UADwVmbq(NrnoRKD^aXqJH9<>&lsUDgF}UPvshCneS%F-7t695KZbEc zIGgiQUf`{kTRSwNU-q=ggYAm#FNkhJKh`cex>QOh@)UuMb4)aRIb)y3-nYZ*xJ*EtvlvNp%n zc`v^tgvekRzYDt4vUE7UQsDHsitFvLfp{t}yI@^bn*0DT!K_Ym48$_=zZervuYB6- zOkTNIl63$(^mc`~L>&si7X4~6H)(@_EpWZMT4Vpl<|zLit~K~o2FxlL{f~wf$I5kQ z-vv1u8!MKL=b?f~xr&e(dDP~a-?<$)?=I6j`hT|h^^*h!xnT9MnD0%G4`wbD{**E` z;88~BQPV}GV*vE)@)jaC&F#0o#z6#KhEA2yc;ie)Gr}(^wre_&gBEMe4D7gTPf?x^ z;0b>G86zKi=l}s(m>`gSM&%j=Bv!`p-GD0id|yf}YQ}BHHQ=LLERL(2=9UPR;fKaD znD_3e`WP%%_sp%vnOB3(4@W;i1Lz=HxU)A>^j7v_1Hv{&jw|M&qCxQWQP*`E^)fO> zcr-4q7$Nv7pmR2XF+h|f+K^#*Q{Gn@~%obTg{{umV+SIM5r z-X~dF#yby){^uB583j(QvuQ z#dfg1V?piNl?Ijdf&IwZ-gf|;=X@-#q<$O^#_Q`|{SM%iv%sxIVz=s3e1~*_y$0b# z+con$<+V6|FR&#(pbk%M*a)N<$~iaZsh$OE3NvBk}HR389VisBdy zI|%7=dh>C)&XY|%~iY7iHB(^f@$bI`r-2Ux-&oW=4a*k0@aauCd zr&$pToeh1Cs)iXDq&WNFsbV%@E7#q{sbR$VS+LU$LnfWxor3BmP}OiZUGn@2*78c^ z<~ZqXTclB_*C5}gJw9i2PFCSj^6Xu3w^fKQ)DJh4c9Cfus}6S*mgK-OD9MqNr<&|S z6koPLhKH0{ui9GQgbP}o7XkE_>kxV?fTh3*3TaR3%&xfc@ls_3Nsy}O8hq}y@GZ#D z_k4GHZm0bzb29V1wDS)B6zyowQ?d5zYdqjEp{fxQh9Y;pU(8g^a2GVZPjOrRcV*AT zdyBI}0IMU^$N^qJk-A1+KeKuAzDiwdvAvMDZG`8w^HdmiEE)PPmCL;Wq(_ExJ?@BU z0-;Bga#sId-RwO=vTNg2nsAq^fFmMI%*s0K=qE2E-PPFMg4OT_-}LUTu<;rpLTH~> zs$P$Os_Dl5CW?J5wc(~b6Hm{0sh+YkE1i00n4rTYyMaklK1{={ACnG*bBvOMlIjr4 z!?fEe%>bDb-a2LOot_9q%=Ay+B*vf25eZ9dutX*2+qBen%Oor%OLr#R&%xd9{SSlWjNdAk^89ZV&Bb#@>eVVXJmCj1&?Om8ggdKlmg=j54Mc} z>R;<(RfXbs=W73t^WI=yLg}d`AA`3{qXW}p5zKH;`mB2=M8*0p5@tgZpWcfzo4kjs zCp;DvkaBtH9pQL*`~Uqu^8fpa`y~5jf9P%$=Z4}u2OP)N`ND$bb4Jm73IS`nqF9g< zeT@h$99^>@f(!$c+uxUtWhvoeSP1qk-*yFT;kw_oUOOC;ltVg@*zu`v3L(WF7O+b3 z5Yl)iGqjhsG&0FSKiA~*yM#MeIH6?Q?Ar?(4Smx(nAja?J@bVGC+-O!0unXWc5eS$ z@gkVcX^YqVc$HMUsMCV1aJ#+QNZ4wM#dcsjmsH|)g*9p)-Ofy#`YGBaqE=uu#QFU5 zorFl_>X8%4sGO0Q)V^i1z!Rw&1J520*_OqK$AZNya(S(<%}L;iota{y4Zuq>8{jgc zhUU29{Jo&>a{z6q5uo7X$o0I%IJKrty6!1C&%SCt2cRx4bMqWoq@&x3HiKo5?*4C`?t6RwkmoET*ct1&@!16TzRFPaVQxqyQ>pSR<4wP4)5G`M9E~OHH zpAY4aqZK^<^hmMQdLNjy zmG$`=zTE`oU?<#FH$SIeHrufv&(ibCp+%z{C{Y?(hudTjwJgHCDe3y+eyuw>{!FDM ztW}4(V|-k*vRc%#7+7syHcuGKW)6s6#pRhXqGqE;3O*Z+oC@W?c0r)(AaTs-yj`gN z9c$}b@`qx5hs1yRXRGe$*W=>hgb+*@k}r_tn48}>6PDxuRT?e7r2V|kPB>hDBrOK5J9d*;Wh>`ox{Ps^)r0tN6c$rZz)RU2vSNty zSpjC&Yu1ifoWBq$)rVOfhZ#JX|2i~%w!}yDM(VX-$0#ZawK!v6*pB+ztD#QMCMT{5 zc}HSE)8PGs#>d-!mUNU3`SUb@t=WG!Ze^cBe4CVZQQB!Ey>2LYmcN#D$}0O6AlL2J zd7nc>l0bseVx#(w2`++7kaAHqp_`!70NB8zKk)_h7*fZkg!jR;3mE5N=vRb5(q(5- zlbE`8r9`y7oQy~zoa_Oer2mG>JM_KvC4_&bM-(*LZT?moKR|a-)TU|IYpBXRkF97c(k4ryYIt4VN5Rhj%=0LMR z65R1bhL3(;^Jk!AxpE%M)$1I-B;*q3YFceGRO@hT0uC!^eOYE3_1r)AGel5 zCZ2)y%CpL){nv1B5xIPumV?*IBt9ZTA+ulR(h=V?1UNxvZK63WLIK0ms9ez@gPro# zFs_DUSHmg&_~RNqVBE@-!Vdq&)MtjP82*;P!#h*0K~z6?p2;CB!WFgS{ z3j!3-S-r@Qv1zDPBK@+ZsC@wDt>eZxnD~ZXN3#hQD zZA-*PBk4-`OG8n4nIS=y}n}=}b#$OwQd# zDf=}5`o|-Ny_egl|Fr5I&=zL=G=8Qcy0*0kH7w7V5J`(_qX~*KVIiZXQ30eY6Rf`{ zDeQNMqp5_2q!Rd5gAT4~qTEvW8GxT`htL89hpsTDqgm2_-|hsV4%0szkXUDZxjylF z5}z%0m7k@=?!&I5rp_?7kUEg3nA*yBObxRV4RHK`YNmog(M~-yTrp&ZRQUV+xipND zLh#*#Q`ccd0WGo@kIvQO{g?M zmhXqIH91Zo1>Fjb+t^HV1kLEjzFypgWn!k3YzTF5@lFRm+ z;_=N<IFYovl>J1>FKNAw*WJjyT>jt*KO5X`zM%&!eSxqvG^xTB(@MRlXB%ull$ejNrW z#3d?RE4pX1hp}MH=q%#GzbraSi5GeD*1s7J9JGnH-KaOHAZY2FCkkd|Xw!%a)5Xjf=Ai`p{ zK;jnn>z_gIrVm?F=@#^@%b)!UD3KwJ_&H%4Vc)uV4AcxwrpA81)A@MXKnGkt5-)^e zKg(ay(xa{t;!Za=)MXZqDeN#>qCQLTxW8fy zngWh!JPV~uQ+LA)1p3$*d4o?HM!L!OZyD&aLHjUCBmgnBUhRkUAWV37 zL|WcbE#gFVDshp@ZgjyV7_p}`qVrjjpE|0h1p`@-lNPe2`F>I_8GS~-Xc(p%;wklm1DM0u%cDR4~$Mn zZ>j*)qYGb`Jm?6Y0|^eJ@WUC+2Fg9HUTQpG`7tO6jPNiy!^kCCCZcDKEajS<6<4y& zBlnl;D5z`kSo~q&X@VYU%L;RTyV0SVRU6~Krx21d9u}xKk*X#v&JKkvqcRD$oV%J>B z*EL)02zpiQ6yPhoNx2@MwEgE}EYE0mYS8qnJXtmmj?3Lt!q7JYGQo zJfZG+s|B3bg-@@Z@}k}h3b3NWR(~T)Q}T_`pYbqKUDuOE$b*H|Sh^#VuB3Vlfbqfh zH4F~pYNRg{M}vS604_}Uv&bu3mJ*pPoq|~qfpf=b+vDfvhe zyv4y0o=O3?9s$uR0@O9p@3F|h0$!E3pB5|s3B_z!N(Y&5V(T~UHQAv6I*OaY<6kWU zGW0R$pK7vBNb)QK|9RXzzlvgm@6#3zxGse`co*-m`1T8}#&^9&l|EI!UOU(WEnxqD z5trRiqRELi%D3pQ!a-sTmH;gWjyV%c+buC0duYJ83?G+L4pD&4LYMCx@6*jA8XQ(4 zOBE7tfC_m@m=?8L!zKZg?&WJlybGUADfJE=Ief|H_XnAa`CxOfxv&+qqIJ;gU%5=P z6Z2dPH9&ph9!$>dW^Y_=V;8Sv&f%_|8TU$JkX8Z&3P*XJG3_LGI$GsQO>^Lgq~k3mHQFk9no0y zW%j$~?G3*%Nxt)-?TWiEP+mS8Fx?~}ySkD+`Lz5dBvlB_tU66XGT$P9s@`mc*Us~| zVKo*ECBpA2!`or#g9>;tC@$BAJOAOiK4wm;nvC)M9T@mlX_BL_D($ zSv7FmiRpX2eV!1&w&(vi(=Te@7t_>V+o(bx7Nv2=Fihjazvao%Il!$dWfaj zzL95^tI$9vsIjk=c`ebkhYe7VZ+33BamQ}E$Nnt`vH7p@8gtAItaF76jz#k(6mU3G z&Kr9)&v$hS8!pvl0~h*#008G_7K#p~2Y9gY@~mHP(N89`J%f9`+9ev1WjS+im*(8) z10>LMXB;&>fCcEWVXHU050qO6?)7bHu$L=~jsodbHk1KLrNVyqCtOuF5VNN7j=6J9 z5Q18DAyOk^s9~$-qnrv^@g7ZY;_Il5nvd%PAOTdLT(%!&aXi%IP@v6SOgbbwm?~ff z2K?pnqiQB{0He|%9=i{8MAz7k^(nvoW%7c0`xc zPCgGGP>^oG5D3OP7}wsZ`t{vox!Ys)6X^FxvV5Ec0?050rA0=v(VKR>TQ5PVOFGwx zRpWH_TZYHgQFOA>CPwomc&h z!TIv-CsUVUAV#$HLu3m6Z~9Ecj1j3p>RJ!|RU3+hkfw)>1zKLv0dC%1l-uUWuu+;0 zU;F^_BKNU3y>A-pT#qm95Pc<_(BOGLgqndm(1T9mJgaSyhGEKn;LB+t zll$#oIhp+6E$VqM=;X;7sJ>l`6L2Llg$hp7-6vv+uEaUiyL2R~n5U zoySfBA^wT^v&IbPT|s}2m=q+f)6oc&({mVdg_o!Z z+;C)~5ew-`bPL@RMaScZ*E2}b!-LRXU7_(>$QL@s8mJdXGpfl4+%+yc!d-yK& zxgp%8J9t^Wa#C*P&b`aCDB$q%UlH>Yyb)zjK-pM*wt(!~DqCw4SUsJ+pBB^wZ3?cT zW5imk&gk70szhs_%?`!n-9H@QfR>>QdBH**j=@m@2Z&*TzC*0f{`#(bmDvG8ucn+; zO8Mm1u9~_f!uASBDg_^tC9LH{_o}DTI4a{T2ERiY+9;D(oXJM!>d*ao>uJX9Ey5Z&u8g%*K@N}|DH(=kFG4F@WP^Q%evUn}{ zdvHRZ2qvbmJrYhzh~M0aXYb?ZRn!Wcy8eNqB%8>axSz}o0009300*((S2T-lQE~DB z`yWUYq?}ta97`Qmh2q68hwNg$F9Mdi-s~K?JrM{hemc%Clv=6cBbH|)76N;MkaS_1 z^|1RFACbU_PLVtQl|}^%bK0=U00094#_^d9;`U43s*|FS!d8W&A&;cj^OqMPb{Plb z(rD(9h%UH^ODTFBpPNlkm^=jmVCNO9sS1W<0lJg!*jrgg!fD62$&&;%vDqRkMkUEn zQX~RBhMHa(Oo*u{UJG4Hu&{y5JJ~3-FSHgUL=GugJgi%yPc~P*yxGM2fMkTig=O4- zQ#qV?AJ#Rwo)#5w&AS-{7=jFWWFUO=vS>RaSVu4VtYdmb6y06h**$~Xav0DHcn8D} zV)Nn1>@{?}OIxKr{2Jqy%<)A2@3>P$3bn1EIYq|^3$A0J zpIMGz@4#Q*oyAxn--roPRyO+mk#4Wy9NgxLi-V)aluCs}Sgntb{BxXZZ-S`o*95)Y zsp)GTr9nPA{;H9EkwQUn-hET(f@mdP*{t{jUnAx|CV0a%0g;)j12f;Zxili(drOK> z&fg@BpBUV&er3J#UxM4OUO!EDF3HZ@CSU>aHp>O`uf2T}k;+_l;<8+FD+Z%QCpcgx zUdo8zgri>wLm@21&kG|jGAN(8o3ewk@OgjT7$f_Y*(wfd3bRNRDaI;j$&>&-u$piE zW1~1cK5t>Y1sDjxxcpt_@R&5XrDFid{MUH?h744$M7*0B^<@bqlK|h3k%Qv>KL9o< zX@o(~Pk09&f2|fXbn(u2hW7^gf6l3pCM1drgg@F<>bKkc9bz74j@eyjxA#_h0dZB+ zMj1onYok*tC;{`wm}>h&+p*n6G9l>vQPvQtYkX+Mu7|lIN{{9FO+Dqkd^j$F?L{++X?{+liuL-aaB#Q-Pkv4%cRsNaa$ipC?o|OVwwN2NIu=FOFFJ zH7DzU;ljCL8@mD|IcSvr;JNJr%)UJw#VUbv+{$~m*xchaW_TI-GxXzN%9HtJ?~0gd zNZ_eIx7@waYbd0OtZ1S}c+j6d*QaO;w5W&$XH%rngqc(h4urq@K}%7d9NW!~vv2_c z(gskR#XXfPn0KG$JefXpgn}J$H$~`Yw%Qd~v(`1T5!?hq}x50&veq30uU3~ldu^2jjaQSPM_$_Ye=y!q6ScAUF8RpRoJ{Dp&f3| zuc+t&XV$Wcn`1PaqH9^4UpO-DUABbH|EE1xjZy1NXX}0K z2wx_;J=rpTG59Kt5TtYzAE4ME=?wQ*3&lSN>JtZC@K|X}(hkELNWbsp?DlDpAs?E4 zIts|Rnf9N=tesDv|Mz+^3G^SDul%1Jq)!IV4_7Zss|$`ltKG87VU>CCEm*sn?BR3! zD?oJI1XDWjiy$=5G}it4{V8K?uD_hVkI%}&5GW-dg9PdhtB>TzW11VRk<)KssQf`r z!cf28Xz>-R;su%wOs$~+u$xf|3&-AL_(ia{Mg6S>fALZM&hMe$n`NNh;#UmuE_Sw( z3I|1(6!j#K+DO%Y6{eOfJq8$6)nq~HuB2j#^_E?93WkrNE@YGgtVrWEi%)FO2@PHp z@7k9A(}9zu9w*<30lpqhaU=L?4N=h%%C0z8L_0%v1^|Jh-G)I6HG!a-jQMW8E7hpp zy{3fIw1^I;Z^`>_UoLxYohT+tiX|{ic7ybe$@b=MnYz)vt#RF@3auSwhd0CGEYkPd zoZW^hjj^nM22VqTG+Amb$%cLQ!cc3u{n>N)_bH4Vpr`cOeKBYcNde#xp)B_KL$`9! z;IIhHUw@G%{gi+E8w6ZtJRXumJi(TOTzOxuo@7rnt^`$Ob*jXJl-X%?3(z%__1~=l z`pA6f`ff0T1}ruN2qdJiZP>48Yh*eZjKK)<)Qwh37W-f_o%D4%vD|gYj@5ctiGAxx zxNJ)%py#6YbCHSk@vH+<{f3+#KEWl!kqgNCszaHxsUuN&0rCofJx-9twzIG+lmD4HS+AvX)@E6qd8$33 zDAOD~l1t660Ce@}<|H0M*<>cW;#FSA%C_%sW5I1yFENW}#A#g+qenY;V7OMnJ$rqk zC>vEPG5_jM+SWM9`eTYTW4<9tZkCGBp~tGxn_X-QufufcC6?I2lZ@QL@nIe^fOb~N zAk#|^17Nz#%76Pwe~5OdL6pvgJuhS!xmC}jvkG|a6+M=SlAo)Xp545qW>)UWfMpCEPHd;iWTECA-;#Y=Qs67$2~l768O}O=z=HE<6*%j1+h=v_ z(sak56IOI#mLCfkCIi^($nQRwI1CZ7d}9T=EPex=_}v60uD`W{(EX^!!B&(_;`FiJ z57vH6k5OSKSxWu zGq@ia9`MH8(7H#qh9Fye%}_S4_8Hi3Jd#768|5PM2;H6Ei~z}cZ_(MT>!%axHE0xp z8tRa8e)Hvn()??xWWu;^Q)>8@{DsV4V9H#87K$6A1BCZab@DsJ zf&bwx!FKNSZ6T%qn~qQGAGdeAq=U6*dSLKRW^gQ&U?qJvEOy7##gNjmZMw;0eFxrc zBP+=I@sDmDm=!$VMyEIU?c+=|!PoTE(2ssyIL14|G`)fJ#_N0tCN_hb>SNO>|NPWY zachXjGY0s}60wM2py|ESOF2w2=smKr210--7A!V1b;048G-#jhH2el7X))5af$286 zgLKG{jWL5tsF5wJ@8^!)I~o15>Tui~qA;{68d_;Zq@lQE&*f(0qybj}nB_&IvK^e9 zAO&F&K1I6>5zv~B29i@~%6Eh<%+f64#K8ri5rBR|V&!9XD0UIq(Nqu%fBQcK5e<+P z3RRs)55GEXNahb`d0535#%+)9*}y&9ui~f2xtNU7zuv{mK9Eoih(YBN35Nz(%Ahs-(JJ;4W`d+4B1rx+;AVN6k<8bXQCbG_#??LO~{ zOn)zsOy^(mg{@x8J|X|(zZXw4jV_6_AevAp*JP^#I%VY!7pi})lJii@VRJ=GMCM!H zTALxt4j3Bvar?+FJpDt+^ZfBGbiGi#+b`OU=B?CiA*ca_NorYM^COoD^J&KTqJFMB zuaZT;$tDB29P=hjzfC2BOoQPTRZ~c<1IY!*`sEW663d*LY15ymBub`inUY5*LG-S( zyf)L4JVma8y|EG}r<6HvRB8adJ2KWN!YVhvSLO4Q9C7UyY?x$QYg0B#8ovj*{HtNxGQL>XKnC1#Q8*Rd*sf zYzGfai$u|e6zng+S=k5UGA^;t3cQfnYI|b4_l?w{a~_@63a)jD=lgL3cen9zdpZNW zU5`U}Meml?2#zpz#!&d=pc~+b6zfs<6$jKsmcDmE)L^y`&-GLrU&gH;U>z4vZ^_Gv z4Yg16V46#};&SV2(!y9b>J)^VF8JGK+>v zJE%~-^40b@`?JdHfiH0YN5AkRDzGDcZOIC+2n z+B*_;s=d};I6+KmBh9gT-mBuY-Nh#j?zC^wp0ZabJ;&=j83XeSi;)vp=MAlRl=3af zm>y>f`deV5E;G$*Z?d6Le`*qDU+zbYv<~9Q@9)L|Ww0WKHU)b;Kd!crj?y+hmbwl9 zgAc3RDADz~vNoo++U>RL&U}O1!lf1*1TDrd*R;B-#PGG&4{+>|&UNU$$BQ6A<`OA# zAc#E4s#gXhPk*qB<1Yr2eeA?8#FQDTCjKS}b~Zz0emYF~x1QMyTyd^bq1U_=`pS^U z{f@-72S;)AlYlFY|G8jMbE%#lw(Mdku|qDH4fb#(IQ4!i`SV${Mm4uZi(fLZwiwZd zjBn2-$AdP{d625=(OlcG7@;EZ{+cWIbgNF2fxgB0ihIfp5xlx~*)zK(sb!A+Jp4e` zKRMxyw_A6jO#}lZe~dk|QfzHkAjj?v4<`jpr3PP(LkT!{$y*!w2x6ebs~y77B0U=X zOb&h>9QSfMp88$9Z^<92V^H>EG>C-hQr8E5344`cEuHnf4q9zJbzD?U*So~hDcubs z-AIR|lDa5@EU+|7?Me#Lh)Z`!C=H^dq<{#LqM)==i{wfOC?W9O#pivW=X-wd`Q5qa z%*mN^=KgnQrje>ks(+13h-j6Wu#5DTYk+=nfm`25`D0o~Y!u?u5{r5;DYvh0a*O66 zg{X@bC5BU>uXWearm16|-Ee-U;HtWD?2paad->IPyifHBIGY-|nLJtR1q_NxyS!`q z-Gq~$9>0uS0VU0E23(^Y4T28)d=D!${MzQk=K^6`HF~u(BD>j&^rzO>QvBk(NkqFu zM#w{)X};%{Ffb+h9@M|L8M}49A2M`^6{qh?U8)J-FmIR{$t{;dmNZX6YF=d+FH{@I zWScmqnyvQiwl826Wtg4{xK7PN%N13xUzNy}&L$1G`~I5L(SmE~EIyfIjDcQ{+d{S) zTk!(p8{yYZNZ*B4NaX7dVJ!kDbPzP9Buse#{fK#&jVLjZR9oxbpzM~bMY`Q$UCu1W z6^OO5p#g~tbQ&xik+C+m;BG?4#cLu+R`p~Cf2Q7;%bQmk%8Q1!;V&HzK?D+^Z$WOU z{#KaB^vgHirRPGQOJ(01YQw0thaxbIiyw53AK!JCB~=}At*%>Qm-O%#_Lmh?x>-pO z%cNa%eeCH-i4gmUD0BPfyXT1JubKjHJ?Cq8HRt^T1E@CB_Vv!1JqECN@0vYz@W)qf z_a74>UqnB|>WbUgIMPD?{}qRfpb5`bMS&%d&p9 z-fM#g4LUD~GA+xVRekH#?ELfvW46svH~SJse|ueVI#gD)=O)r_T?vx|Ss7L(%u~Y; z5^qPMMyLx+Mt&EaBNd#jc%rf>KCcxgk=P(C^P?xaoV)vywr$9-BpqJrK;#{CDo4&pLzAj!!C8J-JTFY;h*5FTY29FxvR$ zUFs<2ffb(JewDAKkZfMeaayj(HKoNFUqCxsik@Fm28y#uF_ za0P!Btgb3gr*V2NVz@`*vZjLj50R%?a&rBQEKLELn=O_SYbbGcSahZ8O$|hC^szgs z%b?;$!Wgiv)U`pSr9IU$SERGNe1yL3!0=6d$4$ytp|#=NK3U?lIl*lvSu=&H{K|)6 zv~|a#oQVO>c>bn|O|EbE$KxvbI|_KGE3k~an*3^&8Qze)kfotq6km7uqIa+5)?W0+ zGb+)L%D8(DV(ses5N!s|au=>q(V@%`eVO-kTo1xN#E=v1HD|!E)0nKMvX$`DNUpWc zP^}04Yp?K*o4Q`AMD-0WDlI8(m?t9T?)2?RWO3f6^TmrwYf(^H*`W2+eeHfJ8XrN@ zu_y9!YZkJ{z#Xa^=&5j)vU_IEX{7S3$Y-48g#LlTt>Q|KJ%;Qbpi7VTtl_%45gHmJo2g$*gZLQz)(0ql-0Afp-_j()YZ_v_7F-lA z+q1FQR@0_%WcKw~;?!#HG7+_d|A=}N0-S?;X;@+F8`4)d%OhmdUs zdGgKoa>(T(^ciKIQ2P^3)|m_8MO@Nf#JL z&*NLBkR3UTTkcD?YT$hzvlO8<-k6$y^YbgcEzfYt?|X@;FVTm*s-^H)8}*wZg!)!UPqO(Ae9uF?js#TGU=OP8h*F%$`GFkMJQs67KC5;Js4ETF1wFj6K)F0 zdtq0K_VY0CH5pTTluEwdE8_I1@BWj6H@z_hj8aT}+nnuzsPVR$5+-n{()>Wousr<5phX?~VGHw@&~ z-`$blXP@>f`#>ys?KjCB(=^5`BYG+Pl8*eS$wcLCBxQ3=jt1(KFzrx2E*_KZefWic<-=xi#8M1FUB^s<5SzAXh7Sf!H3PjJxWCQw%5Fmc z1d)-Qs?%IB?P263((ZW~BN~E`@TP_f^B9PghtxA%7kjuYJx84~r{*C^s2lQelcO^U zoI@~HP6HJO`7mtBTg(W)#7EOt+Qf|FWxO!-7ATa74_Kf?7HLpZeSI?YQImAY@4KM&i_tV)tMp zAO4MGW)RxzD3|bPFlgO+LsD>&S1`fp3xidh5wl`G)vZXm@TBHa_ZtHD@QvyPIqoAl zMni1UDJ~0V7q+gGr7BjP^wEZon#xNX{OOX&g{vIMJG=Vqq|=Y87(dX}Ug4<$j>a*@vS)5GnqDUXXCSE> z>KG{|&r&=qR23SnOH4w{`oEf%|M1h^qnQw)qoA2$p}NKsVP1XO&GyW`==y`?CoDwC zeC?wp#mvvC<09{0o*{xr^2f39fW`CI&8@5|=uMA=f-!I@Og1szu;W+ag~;fTqoj_h zE~gxI5&6S_MM-+^LEi&31Q|v}vumejqB0NZp?U2lly2}oXzLDYwqmBE4aPqx$8Hn; zbxJ4O32hkXR#Jd*^O?uU9A^`R^Rx665YqN>p_N;X|6MYfs-dFy#^%NSV>wNkHnC8wf+*P^uh` zvp%FM`c~wT0~NG5(+!RhT}m7_V5e@d_Q-A{pWbVSiYJ0U4UB1tY{wGNW5AnwGU&T> zISZQYdaYF71^1jJE^m^!MN22t#7qkDGJ`KFg4q2xcI$?(a2^BQ5pr z8Rc9|JNOJ7Y~d;YZShtH0gdv3z~rx%-&V>7FZ5HO?mMdId+iZ~0fRERWXr4Jb1pUu zJXCG|Xx^Zcbj-6)chg!j6slI|ghY?2g`YmOo$cS{aDLJ1xrSy=Iz_t&dSYB2ypvH- zU*JFXZK7u+`5a49HAh0i19q}x<8;*?)y21|vXSE*k9XO-%E+L+qpni`+nDxUoKarv)} zRG*=odk?#O*SEvflzi+x4TqH&a$Vs)azB{u>)HK$Z^zjF;#)emzw?>~wlnC##!5&I zE$E0Pxg>AEM6WAe2{FuJY;nDZ_$ZcI&l~bmMSrdJZDDO*g|wFAFMsWi;aB2wG7C@I zEhJGpW6<;WM;jb6(~bg`>Xt);Z_}+Ah<+I|gsrp0KNm2kC<8x;C+O8p?7-lE7Eh>K zYd)Vlr>tR-GoT+#{kA{JCyc&%fA>k*@6bGX_iLZDebJ=(?{v93R`$PQw|{*W5Dt%m z##_i*TRjRJD+eb}T8X_;cX7Ks(lI)>KfkM>OWJQc4QX1M(=$|FUYUhXvFR0DR#g90 zxHf-)7a*{4;#G*gJCw*hgEn0^jUsKZ`mI3n6j)Dj|h$87D)3e&g*SDTDEJv>{tXo{a<7$d)uRX;B8*ULy_c z0;xs%c(hw4ttewj=+svB2_G*HW3#i+W%=LZtgEib8n>Owa$BW4DhRN6csxA+X^QaL zWCA83uZH0qz?qy4HF$q<*IR>KmusH z*^xw{aHUs<_>hukcdp`}4A=zgdvxCKWBckTv7-naaySq!I#(Bj@hxTQJU2)qaOy%W zeRrs##go1SqB)CCc>mpoD=7Z$PUlHtY=QZjL|(m&T$C+QeqrvR*zR*8hUS0<(ntmA zQyGuJaPpM2=ns66!`U@dyz*+!4D{5@pPXrhXe|#0GS0X#=CJOw+rNIU_WeNlD?RjX z@!pC|L*&4Vxw5d0H$NW6@O96`d@^R+C&f$LkLnd_e_^sqv19)JakJ-c1XDm@OlIaG zU+{F0b$)Ok;lP2D$9?Y^dhqOE=QRvz%ed-hZJOIJI2#@HJ{RM+aZmFGK8alq|Mu0F z-e>A@Pq*-TPCg4~qc}!*f4+Q zFYh^dwf#H%T4!!}7=tO^iXE{XcHOd;Cz3Bh zzzrWu81KQWvv5NBvF!?w(ph%)@UaKHRx(BM%|7jipI*eN569rJ=g_XB=rMbSW^M~x z50;qwdan5F&9I4xTb9%X^c$EK?)Dv;$p$j5Z0bTzQvS&tI@jmPJK%Ac?BN}sy!pE- zRTZr=nnfya$>t(7y*$35pC&*8VB7h|sS;mObrdI0KA){t@i=l={Qm7n5f<>cu}xu0 zQIp(z`SiPAwC;kM=lJA){WslV7|rwE)kG;DjhM_~ZehekQ*#(;Yot-bk6res!%#te zyFj6>=<0z-Guy+i4(2It)7-tQM^@{)e&zJq3XkMi7bE5WZ8-*c9hm2j;m(GOrIOU|SqV z66|*-@?#SrQ0%@o8(&eNr#nfhit+mx)duy6h6sDLl?BH0tBYVMF?X(}Nn1jibt;;f z;6j$t-4n|}{}OzI;Dd@8+qogrf+e`mRTe*NuO&Cv1`PEAYuxFLrnC-H$?^K8HJ=kt zTv|HY^jmsSgc#WC&jtd4%rx>f{KGpzAgkeC*m}fUyGUpgJNo^{=}#NQbotC%psdDZ zIsYl?`I%4ze{^=odhDwxS%j=$~2NY$*f}gQ|g@^B^H` zzMZRv&Bfv)qMQaf0V|R$mtgem>L|#lfGD(To=yc5wcw`?XqX{9J$(TicZ9Dq9Kino z{524Wst^PQ;hw+Y|8)TNzxwKbYyK|^5BLm%Q9ibAfY1u%b`cZ!K>iDVc?0798~zpN zf5pY`0=(n$3Ig#tz)?s5vpKjUQ5T$xk>dP@{xxRs0Rrw~3&^+-@PE%;9net+-Rc3P zAdsjt+}-Db0$#o@aEJfUI1B~)o_e+(aCZls4!98E;^7DobiRlSr~e!_AN-Gu4esNB z)4>5wPJZ9Vo$C(`h9OaQ?f_kbL?QoE5S&h-t*^Z#fN}6opBD`LE@0Yv0Eufj0MlOw z^yQ?)BqhZpB_z1qUF`g&OPg22rUoO0XKuk!~MZ$4i-1rDUAaxu~2h0Ok z2pr(}xF%%(w=S;u0523k0LDpy8vbR=2H4tPaQ?Oh{9b7B0kXiqWH + + Author Affiliation Disambiguation + Trust layer for scientific knowledge graph author and collaboration nodes + + Maya A. Chen + Stanford Bioengineering + + Luis Ortega + UCSF Computational Bio + + Maya Chen + MIT Materials Science + + collaboration edge + + homonym review guard + + Output: author nodes, collaboration edges, schema.org export, curator queue, recommendation suppression guards + diff --git a/knowledge-graph-author-affiliation-disambiguation/docs/requirement-map.md b/knowledge-graph-author-affiliation-disambiguation/docs/requirement-map.md new file mode 100644 index 0000000..fcf84fd --- /dev/null +++ b/knowledge-graph-author-affiliation-disambiguation/docs/requirement-map.md @@ -0,0 +1,16 @@ +# Requirement Map + +This module targets issue `#17` Scientific Knowledge Graph Integration. + +| Issue capability | Implementation | +| --- | --- | +| Parse authors and affiliations from uploaded content | `createAuthorGraph()` accepts author mentions from papers, datasets, notebooks, or protocols and normalizes names, affiliations, ORCID values, email domains, concepts, and DOI evidence. | +| Build author graphs and collaboration maps | `buildCollaborationEdges()` creates weighted collaboration edges from shared document evidence and shared concept context. | +| Aggregate usage contexts for entity pages | Author nodes include mention IDs, source documents, concepts, affiliations, and merge evidence for entity-page rendering. | +| Support semantic graph recommendations | `recommendationGuards` suppresses recommendations when identity confidence is not sufficient, preventing unsafe cross-lab suggestions. | +| Output linked data / schema.org metadata | `toSchemaOrg()` exports creator metadata with schema.org `Person` and `Organization` structures. | +| Human review for ambiguous graph edges | `curatorQueue` captures homonyms and low-confidence merge candidates with transparent scoring reasons. | + +## Distinctness + +This slice is intentionally narrower than broad knowledge graph extractors and navigators. It focuses on author identity, affiliation normalization, homonym safety, and collaboration-edge trust, which are prerequisites for accurate author graphs, lab-to-lab maps, and personalized recommendations. diff --git a/knowledge-graph-author-affiliation-disambiguation/package.json b/knowledge-graph-author-affiliation-disambiguation/package.json new file mode 100644 index 0000000..786d59f --- /dev/null +++ b/knowledge-graph-author-affiliation-disambiguation/package.json @@ -0,0 +1,11 @@ +{ + "name": "knowledge-graph-author-affiliation-disambiguation", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "check": "node --check src/author-affiliation-disambiguation.js && node --check test/author-affiliation-disambiguation.test.js && node --check scripts/demo.js", + "test": "node --test test/author-affiliation-disambiguation.test.js", + "demo": "node scripts/demo.js" + } +} diff --git a/knowledge-graph-author-affiliation-disambiguation/scripts/demo.js b/knowledge-graph-author-affiliation-disambiguation/scripts/demo.js new file mode 100644 index 0000000..3f9c374 --- /dev/null +++ b/knowledge-graph-author-affiliation-disambiguation/scripts/demo.js @@ -0,0 +1,25 @@ +const mentions = require("../data/sample-mentions.json"); +const {createAuthorGraph, toSchemaOrg} = require("../src/author-affiliation-disambiguation"); + +const graph = createAuthorGraph(mentions); + +console.log("Author nodes"); +console.table(graph.authorNodes.map((node) => ({ + id: node.id, + name: node.name, + mentions: node.mentionIds.length, + documents: node.documents.length, + concepts: node.concepts.length +}))); + +console.log("\nCollaboration edges"); +console.table(graph.collaborationEdges); + +console.log("\nCurator queue"); +console.table(graph.curatorQueue); + +console.log("\nRecommendation guards"); +console.log(JSON.stringify(graph.recommendationGuards, null, 2)); + +console.log("\nSchema.org export"); +console.log(JSON.stringify(toSchemaOrg(graph), null, 2)); diff --git a/knowledge-graph-author-affiliation-disambiguation/src/author-affiliation-disambiguation.js b/knowledge-graph-author-affiliation-disambiguation/src/author-affiliation-disambiguation.js new file mode 100644 index 0000000..0102a5d --- /dev/null +++ b/knowledge-graph-author-affiliation-disambiguation/src/author-affiliation-disambiguation.js @@ -0,0 +1,247 @@ +const crypto = require("crypto"); + +const STOPWORDS = new Set(["dr", "prof", "phd", "md"]); +const AFFILIATION_ALIASES = new Map([ + ["univ", "university"], + ["dept", "department"], + ["mit", "massachusetts institute of technology"], + ["ucsf", "university of california san francisco"] +]); + +function normalizeText(value) { + return String(value || "") + .toLowerCase() + .replace(/&/g, " and ") + .replace(/[^a-z0-9\s.-]/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function normalizeName(name) { + const tokens = normalizeText(name) + .replace(/\./g, " ") + .split(" ") + .filter((token) => token && !STOPWORDS.has(token)); + return { + normalized: tokens.join(" "), + tokens, + initials: tokens.map((token) => token[0]).join(""), + surname: tokens.at(-1) || "" + }; +} + +function normalizeAffiliation(affiliation) { + const tokens = normalizeText(affiliation) + .split(" ") + .map((token) => token.replace(/\./g, "")) + .flatMap((token) => (AFFILIATION_ALIASES.has(token) ? AFFILIATION_ALIASES.get(token).split(" ") : [token])) + .filter(Boolean); + return [...new Set(tokens)].sort(); +} + +function emailDomain(email) { + const parts = String(email || "").toLowerCase().split("@"); + return parts.length === 2 ? parts[1] : ""; +} + +function jaccard(left, right) { + const a = new Set(left); + const b = new Set(right); + if (a.size === 0 && b.size === 0) { + return 0; + } + const intersection = [...a].filter((item) => b.has(item)).length; + return intersection / new Set([...a, ...b]).size; +} + +function conceptOverlap(left, right) { + return jaccard((left.concepts || []).map(normalizeText), (right.concepts || []).map(normalizeText)); +} + +function nameSimilarity(left, right) { + const a = normalizeName(left.name); + const b = normalizeName(right.name); + if (a.normalized === b.normalized) { + return 1; + } + if (a.surname && a.surname === b.surname && (a.initials[0] === b.initials[0] || a.tokens[0] === b.tokens[0])) { + return 0.82; + } + return jaccard(a.tokens, b.tokens); +} + +function compareMentions(left, right) { + if (left.orcid && right.orcid && left.orcid === right.orcid) { + return { + score: 0.99, + decision: "same-author", + reasons: ["orcid-exact-match"] + }; + } + + const nameScore = nameSimilarity(left, right); + const affiliationScore = jaccard(normalizeAffiliation(left.affiliation), normalizeAffiliation(right.affiliation)); + const domainScore = emailDomain(left.email) && emailDomain(left.email) === emailDomain(right.email) ? 1 : 0; + const topicScore = conceptOverlap(left, right); + const score = Number((nameScore * 0.45 + affiliationScore * 0.25 + domainScore * 0.2 + topicScore * 0.1).toFixed(3)); + + const reasons = [ + `name:${nameScore.toFixed(2)}`, + `affiliation:${affiliationScore.toFixed(2)}`, + `email-domain:${domainScore.toFixed(2)}`, + `concepts:${topicScore.toFixed(2)}` + ]; + + if (score >= 0.7 || (nameScore >= 0.82 && affiliationScore >= 0.35 && topicScore >= 0.25)) { + return {score, decision: "same-author", reasons}; + } + if (nameScore >= 0.82 && affiliationScore < 0.18 && domainScore === 0) { + return {score, decision: "homonym-review", reasons}; + } + if (score >= 0.5) { + return {score, decision: "needs-review", reasons}; + } + return {score, decision: "different-author", reasons}; +} + +function clusterMentions(mentions) { + const clusters = []; + const reviewQueue = []; + + for (const mention of mentions) { + let best = null; + for (const cluster of clusters) { + const comparisons = cluster.mentions.map((existing) => compareMentions(existing, mention)); + const strongest = comparisons.reduce((max, item) => (item.score > max.score ? item : max), {score: 0}); + if (!best || strongest.score > best.comparison.score) { + best = {cluster, comparison: strongest}; + } + } + + if (best && best.comparison.decision === "same-author") { + best.cluster.mentions.push(mention); + best.cluster.evidence.push({mentionId: mention.mentionId, score: best.comparison.score, reasons: best.comparison.reasons}); + } else { + const clusterId = `author-${clusters.length + 1}`; + clusters.push({ + clusterId, + canonicalName: mention.name, + canonicalAffiliation: mention.affiliation, + mentions: [mention], + evidence: [{mentionId: mention.mentionId, score: 1, reasons: ["cluster-seed"]}] + }); + } + + if (best && best.comparison.decision !== "same-author" && best.comparison.score >= 0.45) { + reviewQueue.push({ + leftClusterId: best.cluster.clusterId, + mentionId: mention.mentionId, + decision: best.comparison.decision, + score: best.comparison.score, + reasons: best.comparison.reasons + }); + } + } + + return {clusters, reviewQueue}; +} + +function buildCollaborationEdges(clusters) { + const byDocument = new Map(); + for (const cluster of clusters) { + for (const mention of cluster.mentions) { + if (!byDocument.has(mention.documentId)) { + byDocument.set(mention.documentId, []); + } + byDocument.get(mention.documentId).push({cluster, mention}); + } + } + + const edges = new Map(); + for (const [documentId, authors] of byDocument.entries()) { + for (let i = 0; i < authors.length; i += 1) { + for (let j = i + 1; j < authors.length; j += 1) { + const pair = [authors[i].cluster.clusterId, authors[j].cluster.clusterId].sort(); + const edgeId = pair.join("--"); + const edge = edges.get(edgeId) || { + edgeId, + source: pair[0], + target: pair[1], + documents: [], + sharedConcepts: new Set(), + confidence: 0 + }; + edge.documents.push(documentId); + for (const concept of [...(authors[i].mention.concepts || []), ...(authors[j].mention.concepts || [])]) { + edge.sharedConcepts.add(normalizeText(concept)); + } + edge.confidence = Math.min(1, edge.confidence + 0.25); + edges.set(edgeId, edge); + } + } + } + + return [...edges.values()].map((edge) => ({ + ...edge, + sharedConcepts: [...edge.sharedConcepts].sort(), + confidence: Number(edge.confidence.toFixed(2)) + })); +} + +function createAuthorGraph(mentions) { + const {clusters, reviewQueue} = clusterMentions(mentions); + const collaborationEdges = buildCollaborationEdges(clusters); + const graphHash = crypto + .createHash("sha256") + .update(JSON.stringify({clusters, reviewQueue, collaborationEdges})) + .digest("hex"); + + return { + generatedAt: new Date("2026-05-15T00:00:00.000Z").toISOString(), + graphHash, + authorNodes: clusters.map((cluster) => ({ + id: cluster.clusterId, + name: cluster.canonicalName, + affiliation: cluster.canonicalAffiliation, + mentionIds: cluster.mentions.map((mention) => mention.mentionId), + documents: [...new Set(cluster.mentions.map((mention) => mention.documentId))].sort(), + concepts: [...new Set(cluster.mentions.flatMap((mention) => mention.concepts || []).map(normalizeText))].sort(), + evidence: cluster.evidence + })), + collaborationEdges, + curatorQueue: reviewQueue, + recommendationGuards: reviewQueue.map((item) => ({ + reason: "author-identity-uncertain", + action: "suppress cross-lab collaboration recommendations until reviewed", + mentionId: item.mentionId, + candidateClusterId: item.leftClusterId, + score: item.score + })) + }; +} + +function toSchemaOrg(graph) { + return { + "@context": "https://schema.org", + "@type": "Dataset", + name: "SCIBASE author-affiliation disambiguation graph", + identifier: graph.graphHash, + creator: graph.authorNodes.map((node) => ({ + "@type": "Person", + name: node.name, + affiliation: { + "@type": "Organization", + name: node.affiliation + }, + sameAs: node.mentionIds + })) + }; +} + +module.exports = { + compareMentions, + createAuthorGraph, + normalizeAffiliation, + normalizeName, + toSchemaOrg +}; diff --git a/knowledge-graph-author-affiliation-disambiguation/test/author-affiliation-disambiguation.test.js b/knowledge-graph-author-affiliation-disambiguation/test/author-affiliation-disambiguation.test.js new file mode 100644 index 0000000..dc4a598 --- /dev/null +++ b/knowledge-graph-author-affiliation-disambiguation/test/author-affiliation-disambiguation.test.js @@ -0,0 +1,42 @@ +const assert = require("assert/strict"); +const test = require("node:test"); +const mentions = require("../data/sample-mentions.json"); +const { + compareMentions, + createAuthorGraph, + normalizeAffiliation, + normalizeName, + toSchemaOrg +} = require("../src/author-affiliation-disambiguation"); + +test("normalizes author names and affiliation aliases", () => { + assert.deepEqual(normalizeName("Dr. Maya A. Chen").tokens, ["maya", "a", "chen"]); + assert.ok(normalizeAffiliation("Dept. of Bioengineering, Stanford Univ.").includes("university")); +}); + +test("matches same author mentions with strong evidence", () => { + const result = compareMentions(mentions[0], mentions[1]); + assert.equal(result.decision, "same-author"); + assert.ok(result.score >= 0.72); +}); + +test("sends homonyms to curator review instead of merging", () => { + const result = compareMentions(mentions[0], mentions[3]); + assert.equal(result.decision, "homonym-review"); + assert.ok(result.score < 0.72); +}); + +test("builds author nodes, collaboration edges, and recommendation guards", () => { + const graph = createAuthorGraph(mentions); + assert.equal(graph.authorNodes.length, 3); + assert.equal(graph.collaborationEdges.length, 1); + assert.equal(graph.curatorQueue.length, 1); + assert.equal(graph.recommendationGuards[0].reason, "author-identity-uncertain"); +}); + +test("exports schema.org-compatible creator metadata", () => { + const graph = createAuthorGraph(mentions); + const schema = toSchemaOrg(graph); + assert.equal(schema["@context"], "https://schema.org"); + assert.equal(schema.creator.length, graph.authorNodes.length); +});