@parseo/appraisals 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +35 -0
  2. package/dist/form-1004mc/extract-checkboxes.d.ts +43 -0
  3. package/dist/form-1004mc/extract-checkboxes.d.ts.map +1 -0
  4. package/dist/form-1004mc/extract-checkboxes.js +145 -0
  5. package/dist/form-1004mc/index.d.ts +3 -0
  6. package/dist/form-1004mc/index.d.ts.map +1 -0
  7. package/dist/form-1004mc/index.js +1 -0
  8. package/dist/form-1004mc/parse-page1.d.ts +8 -0
  9. package/dist/form-1004mc/parse-page1.d.ts.map +1 -0
  10. package/dist/form-1004mc/parse-page1.js +760 -0
  11. package/dist/form-1004mc/parse-sales.d.ts +6 -0
  12. package/dist/form-1004mc/parse-sales.d.ts.map +1 -0
  13. package/dist/form-1004mc/parse-sales.js +505 -0
  14. package/dist/form-1004mc/parser.d.ts +5 -0
  15. package/dist/form-1004mc/parser.d.ts.map +1 -0
  16. package/dist/form-1004mc/parser.js +437 -0
  17. package/dist/form-1004mc/types.d.ts +302 -0
  18. package/dist/form-1004mc/types.d.ts.map +1 -0
  19. package/dist/form-1004mc/types.js +1 -0
  20. package/dist/form-1073/index.d.ts +3 -0
  21. package/dist/form-1073/index.d.ts.map +1 -0
  22. package/dist/form-1073/index.js +1 -0
  23. package/dist/form-1073/parse-page1.d.ts +8 -0
  24. package/dist/form-1073/parse-page1.d.ts.map +1 -0
  25. package/dist/form-1073/parse-page1.js +704 -0
  26. package/dist/form-1073/parse-page2.d.ts +6 -0
  27. package/dist/form-1073/parse-page2.d.ts.map +1 -0
  28. package/dist/form-1073/parse-page2.js +438 -0
  29. package/dist/form-1073/parse-sales.d.ts +7 -0
  30. package/dist/form-1073/parse-sales.d.ts.map +1 -0
  31. package/dist/form-1073/parse-sales.js +477 -0
  32. package/dist/form-1073/parser.d.ts +5 -0
  33. package/dist/form-1073/parser.d.ts.map +1 -0
  34. package/dist/form-1073/parser.js +102 -0
  35. package/dist/form-1073/types.d.ts +300 -0
  36. package/dist/form-1073/types.d.ts.map +1 -0
  37. package/dist/form-1073/types.js +1 -0
  38. package/dist/index.d.ts +13 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +9 -0
  41. package/dist/richer-values/index.d.ts +3 -0
  42. package/dist/richer-values/index.d.ts.map +1 -0
  43. package/dist/richer-values/index.js +1 -0
  44. package/dist/richer-values/parser.d.ts +5 -0
  45. package/dist/richer-values/parser.d.ts.map +1 -0
  46. package/dist/richer-values/parser.js +1067 -0
  47. package/dist/richer-values/types.d.ts +225 -0
  48. package/dist/richer-values/types.d.ts.map +1 -0
  49. package/dist/richer-values/types.js +1 -0
  50. package/package.json +24 -0
@@ -0,0 +1,6 @@
1
+ import type { TextLine } from "@parseo/shared";
2
+ import type { SalesComparisonSection, ReconciliationSection, CostApproachSection } from "./types.js";
3
+ export declare function parseSalesComparisonSection(lines: TextLine[], compStartNum?: number): SalesComparisonSection;
4
+ export declare function parseReconciliationSection(lines: TextLine[]): ReconciliationSection;
5
+ export declare function parseCostApproachSection(lines: TextLine[]): CostApproachSection;
6
+ //# sourceMappingURL=parse-sales.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-sales.d.ts","sourceRoot":"","sources":["../../src/form-1004mc/parse-sales.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EACV,sBAAsB,EAEtB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAoOpB,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,QAAQ,EAAE,EACjB,YAAY,GAAE,MAAU,GACvB,sBAAsB,CA6HxB;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,qBAAqB,CA2EnF;AAID,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,mBAAmB,CAoF/E"}
@@ -0,0 +1,505 @@
1
+ import { toBBox } from "@parseo/shared";
2
+ // ── Utilities ─────────────────────────────────────────────────────────────
3
+ function parseNum(raw) {
4
+ if (!raw)
5
+ return null;
6
+ const cleaned = raw.replace(/[$,%]/g, "").replace(/,/g, "").trim();
7
+ if (!cleaned || /^n\/?a$/i.test(cleaned))
8
+ return null;
9
+ const n = Number(cleaned);
10
+ return Number.isNaN(n) ? null : n;
11
+ }
12
+ function parseCurrency(raw) {
13
+ const match = raw.match(/\$?\s*([\d,]+(?:\.\d+)?)/);
14
+ return match ? parseNum(match[1]) : null;
15
+ }
16
+ // Column x-boundaries for Form 1004 sales comparison grid
17
+ // Subject ~131, Comp1 ~197, Comp2 ~328, Comp3 ~459
18
+ // Adjustment columns follow each comp description
19
+ const SUBJECT_X = { min: 120, max: 195 };
20
+ const COMP_COLS = [
21
+ { desc: { min: 195, max: 295 }, adj: { min: 295, max: 325 } }, // Comp 1
22
+ { desc: { min: 325, max: 430 }, adj: { min: 430, max: 457 } }, // Comp 2
23
+ { desc: { min: 457, max: 560 }, adj: { min: 560, max: 600 } }, // Comp 3
24
+ ];
25
+ function segInRange(line, min, max) {
26
+ const segs = line.segments.filter((s) => s.x >= min && s.x < max);
27
+ return segs.map((s) => s.text.trim()).join(" ").trim();
28
+ }
29
+ function segWithBBox(line, min, max) {
30
+ const seg = line.segments.find((s) => s.x >= min && s.x < max);
31
+ return { text: seg ? seg.text.trim() : "", seg: seg ?? null };
32
+ }
33
+ function findLine(lines, pattern) {
34
+ return lines.find((l) => pattern.test(l.fullText));
35
+ }
36
+ // ── Parse comparable from a column ────────────────────────────────────────
37
+ function parseComparable(lines, compNum, col) {
38
+ const bb = {};
39
+ const prefix = `comp${compNum}`;
40
+ function getDesc(line) {
41
+ return line ? segInRange(line, col.desc.min, col.adj.min) : "";
42
+ }
43
+ function getAdj(line) {
44
+ if (!line)
45
+ return null;
46
+ const text = segInRange(line, col.adj.min, col.adj.min + 60);
47
+ return parseNum(text);
48
+ }
49
+ function getDescWithBB(line, key) {
50
+ if (!line)
51
+ return "";
52
+ const r = segWithBBox(line, col.desc.min, col.adj.min);
53
+ if (r.seg)
54
+ bb[`${prefix}_${key}`] = toBBox(r.seg, line);
55
+ return r.text;
56
+ }
57
+ const addrLine = findLine(lines, /^Address/i);
58
+ const address = getDescWithBB(addrLine, "address");
59
+ // City is on the next line after address
60
+ const addrIdx = addrLine ? lines.indexOf(addrLine) : -1;
61
+ const cityLine = addrIdx >= 0 ? lines[addrIdx + 1] : undefined;
62
+ const cityVal = cityLine ? segInRange(cityLine, col.desc.min, col.adj.min) : "";
63
+ const proxLine = findLine(lines, /^Proximity to Subject/i);
64
+ const proximityToSubject = getDescWithBB(proxLine, "proximity");
65
+ const priceLine = findLine(lines, /^Sale Price\b/i);
66
+ let salePrice = null;
67
+ if (priceLine) {
68
+ // Price segments have $ and number in separate segments
69
+ const priceText = segInRange(priceLine, col.desc.min - 30, col.adj.min);
70
+ salePrice = parseCurrency(priceText);
71
+ const pSeg = segWithBBox(priceLine, col.desc.min, col.adj.min);
72
+ if (pSeg.seg)
73
+ bb[`${prefix}_salePrice`] = toBBox(pSeg.seg, priceLine);
74
+ }
75
+ const ppsLine = findLine(lines, /^Sale Price\/Gross Liv/i);
76
+ let salePricePerSqft = null;
77
+ if (ppsLine) {
78
+ const txt = segInRange(ppsLine, col.desc.min - 30, col.adj.min);
79
+ const m = txt.match(/([\d,.]+)\s*sq\.?\s*ft/i);
80
+ if (m)
81
+ salePricePerSqft = parseNum(m[1]);
82
+ }
83
+ const dsLine = findLine(lines, /^Data Source\(s\)/i);
84
+ const dataSources = getDescWithBB(dsLine, "dataSources");
85
+ const vsLine = findLine(lines, /^Verification Source/i);
86
+ const verificationSources = getDesc(vsLine);
87
+ const sfLine = findLine(lines, /^Sales or Financing/i);
88
+ const salesOrFinancing = getDesc(sfLine);
89
+ const concLine = findLine(lines, /^Concessions/i);
90
+ const concessions = getDesc(concLine);
91
+ const dateLine = findLine(lines, /^Date of Sale\/Time/i);
92
+ const dateOfSaleTime = getDescWithBB(dateLine, "dateOfSale");
93
+ const locLine = findLine(lines, /^Location\b/i);
94
+ const location = getDesc(locLine);
95
+ const feeLine = findLine(lines, /^Leasehold\/Fee Simple/i);
96
+ const leaseholdFeeSimple = getDesc(feeLine);
97
+ const siteLine = findLine(lines, /^Site\b/i);
98
+ const site = getDesc(siteLine);
99
+ const siteAdjustment = getAdj(siteLine);
100
+ const viewLine = findLine(lines, /^View\b/i);
101
+ const viewVal = getDesc(viewLine);
102
+ const viewAdjustment = getAdj(viewLine);
103
+ const styleLine = findLine(lines, /^Design \(Style\)/i);
104
+ const designStyle = getDesc(styleLine);
105
+ const qualLine = findLine(lines, /^Quality of Construction/i);
106
+ const qualityOfConstruction = getDesc(qualLine);
107
+ const ageLine = findLine(lines, /^Actual Age\b/i);
108
+ const actualAge = parseNum(getDesc(ageLine));
109
+ const ageAdjustment = getAdj(ageLine);
110
+ const condLine = findLine(lines, /^Condition\b/i);
111
+ const condition = getDescWithBB(condLine, "condition");
112
+ // Room count line
113
+ const rcLine = findLine(lines, /^Room Count/i);
114
+ let roomCountTotal = null, roomCountBedrooms = null, roomCountBaths = null, roomCountAdjustment = null;
115
+ if (rcLine) {
116
+ const text = segInRange(rcLine, col.desc.min, col.adj.min);
117
+ const parts = text.split(/\s+/).map((s) => parseNum(s)).filter((n) => n !== null);
118
+ if (parts.length >= 3) {
119
+ roomCountTotal = parts[0];
120
+ roomCountBedrooms = parts[1];
121
+ roomCountBaths = parts[2];
122
+ }
123
+ roomCountAdjustment = getAdj(rcLine);
124
+ }
125
+ const glaLine = findLine(lines, /^Gross Living Area/i);
126
+ let grossLivingArea = null, grossLivingAreaAdjustment = null;
127
+ if (glaLine) {
128
+ const text = segInRange(glaLine, col.desc.min - 30, col.adj.min);
129
+ const m = text.match(/([\d,]+)\s*sq/i);
130
+ if (m)
131
+ grossLivingArea = parseNum(m[1]);
132
+ grossLivingAreaAdjustment = getAdj(glaLine);
133
+ const glaSeg = segWithBBox(glaLine, col.desc.min, col.adj.min);
134
+ if (glaSeg.seg)
135
+ bb[`${prefix}_gla`] = toBBox(glaSeg.seg, glaLine);
136
+ }
137
+ const bfLine = findLine(lines, /^Basement & Finished/i);
138
+ const basementFinished = getDesc(bfLine);
139
+ const fuLine = findLine(lines, /^Functional Utility/i);
140
+ const functionalUtility = getDesc(fuLine);
141
+ const hcLine = findLine(lines, /^Heating\/Cooling/i);
142
+ const heatingCooling = getDesc(hcLine);
143
+ const eeLine = findLine(lines, /^Energy Efficient/i);
144
+ const energyEfficientItems = getDesc(eeLine);
145
+ const gcLine = findLine(lines, /^Garage\/Carport/i);
146
+ const garageCarport = getDesc(gcLine);
147
+ const garageCarportAdjustment = getAdj(gcLine);
148
+ const ppLine = findLine(lines, /^Porch\/Patio\/Deck/i);
149
+ const porchPatioDeck = getDesc(ppLine);
150
+ const poolLine = findLine(lines, /^POOL\b/i);
151
+ const poolVal = getDesc(poolLine);
152
+ const poolAdjustment = getAdj(poolLine);
153
+ const olpLine = findLine(lines, /^ORIGINAL LIST PRICE/i);
154
+ const originalListPrice = getDesc(olpLine);
155
+ // Net/Gross adjustment
156
+ const netLine = findLine(lines, /^Net Adjustment \(Total\)/i);
157
+ let netAdjustmentTotal = null;
158
+ if (netLine) {
159
+ const text = segInRange(netLine, col.adj.min - 10, col.adj.min + 60);
160
+ netAdjustmentTotal = parseNum(text);
161
+ }
162
+ const adjPriceLine = findLine(lines, /^Adjusted Sale Price/i);
163
+ let netAdjustmentPercent = null;
164
+ if (adjPriceLine) {
165
+ const text = segInRange(adjPriceLine, col.desc.min, col.adj.min + 20);
166
+ const pctMatch = text.match(/([\d.]+)\s*%/);
167
+ if (pctMatch)
168
+ netAdjustmentPercent = parseNum(pctMatch[1]);
169
+ }
170
+ const grossLine = findLine(lines, /^of Comparables/i);
171
+ let grossAdjustmentPercent = null, adjustedSalePrice = null;
172
+ if (grossLine) {
173
+ const text = segInRange(grossLine, col.desc.min, col.adj.min + 60);
174
+ const pctMatch = text.match(/([\d.]+)\s*%/);
175
+ if (pctMatch)
176
+ grossAdjustmentPercent = parseNum(pctMatch[1]);
177
+ const priceMatch = text.match(/\$\s*([\d,]+)/);
178
+ if (priceMatch)
179
+ adjustedSalePrice = parseNum(priceMatch[1]);
180
+ }
181
+ return {
182
+ number: compNum,
183
+ address: address + (cityVal ? `, ${cityVal}` : ""),
184
+ proximityToSubject, salePrice, salePricePerSqft, dataSources, verificationSources,
185
+ salesOrFinancing, concessions, dateOfSaleTime, location, leaseholdFeeSimple,
186
+ site, siteAdjustment, view: viewVal, viewAdjustment, designStyle, qualityOfConstruction,
187
+ actualAge, ageAdjustment, condition,
188
+ roomCountTotal, roomCountBedrooms, roomCountBaths, roomCountAdjustment,
189
+ grossLivingArea, grossLivingAreaAdjustment, basementFinished, functionalUtility,
190
+ heatingCooling, energyEfficientItems, garageCarport, garageCarportAdjustment,
191
+ porchPatioDeck, pool: poolVal, poolAdjustment, originalListPrice,
192
+ netAdjustmentTotal, netAdjustmentPercent, grossAdjustmentPercent, adjustedSalePrice,
193
+ boundingBoxes: bb,
194
+ };
195
+ }
196
+ // ── Sales Comparison ──────────────────────────────────────────────────────
197
+ export function parseSalesComparisonSection(lines, compStartNum = 1) {
198
+ const bb = {};
199
+ // Active listings / comparable sales ranges
200
+ const alLine = findLine(lines, /comparable properties currently offered/i);
201
+ let activeListingsLow = null, activeListingsHigh = null;
202
+ if (alLine) {
203
+ const m = alLine.fullText.match(/\$\s*([\d,]+).*?to\s*\$\s*([\d,]+)/i);
204
+ if (m) {
205
+ activeListingsLow = parseNum(m[1]);
206
+ activeListingsHigh = parseNum(m[2]);
207
+ }
208
+ }
209
+ const csLine = findLine(lines, /comparable sales in the subject/i);
210
+ let comparableSalesLow = null, comparableSalesHigh = null;
211
+ if (csLine) {
212
+ const m = csLine.fullText.match(/\$\s*([\d,]+).*?to\s*\$\s*([\d,]+)/i);
213
+ if (m) {
214
+ comparableSalesLow = parseNum(m[1]);
215
+ comparableSalesHigh = parseNum(m[2]);
216
+ }
217
+ }
218
+ // Subject column
219
+ const subBB = {};
220
+ function getSubject(line) {
221
+ return line ? segInRange(line, SUBJECT_X.min, SUBJECT_X.max) : "";
222
+ }
223
+ function getSubjectWithBB(line, key) {
224
+ if (!line)
225
+ return "";
226
+ const r = segWithBBox(line, SUBJECT_X.min, SUBJECT_X.max);
227
+ if (r.seg)
228
+ subBB[key] = toBBox(r.seg, line);
229
+ return r.text;
230
+ }
231
+ const addrLine = findLine(lines, /^Address/i);
232
+ const subjAddress = getSubjectWithBB(addrLine, "address");
233
+ const priceLine = findLine(lines, /^Sale Price\b/i);
234
+ const subjPrice = priceLine ? parseCurrency(segInRange(priceLine, SUBJECT_X.min - 20, SUBJECT_X.max)) : null;
235
+ const ppsLine = findLine(lines, /^Sale Price\/Gross Liv/i);
236
+ let subjPPS = null;
237
+ if (ppsLine) {
238
+ const m = segInRange(ppsLine, SUBJECT_X.min, SUBJECT_X.max).match(/([\d,.]+)/);
239
+ if (m)
240
+ subjPPS = parseNum(m[1]);
241
+ }
242
+ const subject = {
243
+ address: subjAddress,
244
+ salePrice: subjPrice,
245
+ salePricePerSqft: subjPPS,
246
+ location: getSubject(findLine(lines, /^Location\b/i)),
247
+ leaseholdFeeSimple: getSubject(findLine(lines, /^Leasehold\/Fee/i)),
248
+ site: getSubject(findLine(lines, /^Site\b/i)),
249
+ view: getSubject(findLine(lines, /^View\b/i)),
250
+ designStyle: getSubject(findLine(lines, /^Design \(Style\)/i)),
251
+ qualityOfConstruction: getSubject(findLine(lines, /^Quality of Construction/i)),
252
+ actualAge: parseNum(getSubject(findLine(lines, /^Actual Age/i))),
253
+ condition: getSubjectWithBB(findLine(lines, /^Condition\b/i), "condition"),
254
+ roomCountTotal: null,
255
+ roomCountBedrooms: null,
256
+ roomCountBaths: null,
257
+ grossLivingArea: null,
258
+ basementFinished: getSubject(findLine(lines, /^Basement & Finished/i)),
259
+ functionalUtility: getSubject(findLine(lines, /^Functional Utility/i)),
260
+ heatingCooling: getSubject(findLine(lines, /^Heating\/Cooling/i)),
261
+ energyEfficientItems: getSubject(findLine(lines, /^Energy Efficient/i)),
262
+ garageCarport: getSubject(findLine(lines, /^Garage\/Carport/i)),
263
+ porchPatioDeck: getSubject(findLine(lines, /^Porch\/Patio/i)),
264
+ pool: getSubject(findLine(lines, /^POOL\b/i)),
265
+ originalListPrice: getSubject(findLine(lines, /^ORIGINAL LIST PRICE/i)),
266
+ boundingBoxes: subBB,
267
+ };
268
+ // Parse room count for subject
269
+ const rcLine = findLine(lines, /^Room Count/i);
270
+ if (rcLine) {
271
+ const text = segInRange(rcLine, SUBJECT_X.min, SUBJECT_X.max);
272
+ const parts = text.split(/\s+/).map((s) => parseNum(s)).filter((n) => n !== null);
273
+ if (parts.length >= 3) {
274
+ subject.roomCountTotal = parts[0];
275
+ subject.roomCountBedrooms = parts[1];
276
+ subject.roomCountBaths = parts[2];
277
+ }
278
+ }
279
+ const glaLine = findLine(lines, /^Gross Living Area/i);
280
+ if (glaLine) {
281
+ const m = segInRange(glaLine, SUBJECT_X.min, SUBJECT_X.max).match(/([\d,]+)/);
282
+ if (m)
283
+ subject.grossLivingArea = parseNum(m[1]);
284
+ }
285
+ // Parse 3 comparables
286
+ const comparables = [];
287
+ for (let i = 0; i < 3; i++) {
288
+ comparables.push(parseComparable(lines, compStartNum + i, COMP_COLS[i]));
289
+ }
290
+ // Prior sale analysis
291
+ const priorLine = findLine(lines, /Analysis of prior sale or transfer/i);
292
+ let priorSaleAnalysis = "";
293
+ if (priorLine) {
294
+ const idx = lines.indexOf(priorLine);
295
+ const parts = [];
296
+ const valueSeg = priorLine.segments.find((s) => s.x > 280);
297
+ if (valueSeg)
298
+ parts.push(valueSeg.text.trim());
299
+ for (let i = idx + 1; i < lines.length; i++) {
300
+ if (/^Summary of Sales|^Analysis\/Comments|^Indicated Value/i.test(lines[i].fullText))
301
+ break;
302
+ const t = lines[i].fullText.trim();
303
+ if (t && !/^AFFECT FINAL/.test(t) || /AFFECT FINAL/.test(t))
304
+ parts.push(t);
305
+ }
306
+ priorSaleAnalysis = parts.join(" ").trim();
307
+ }
308
+ // Summary
309
+ const sumLine = findLine(lines, /^Summary of Sales Comparison|^Analysis\/Comments/i);
310
+ let summaryOfSalesComparison = "";
311
+ if (sumLine) {
312
+ const idx = lines.indexOf(sumLine);
313
+ const parts = [];
314
+ const valueSeg = sumLine.segments.find((s) => s.x > 150);
315
+ if (valueSeg)
316
+ parts.push(valueSeg.text.trim());
317
+ for (let i = idx + 1; i < lines.length; i++) {
318
+ if (/^Indicated Value|^AS-IS APPRAISED/i.test(lines[i].fullText))
319
+ break;
320
+ const t = lines[i].fullText.trim();
321
+ if (t)
322
+ parts.push(t);
323
+ }
324
+ summaryOfSalesComparison = parts.join(" ").trim();
325
+ if (sumLine.segments[0])
326
+ bb.summaryOfSalesComparison = toBBox(sumLine.segments[0], sumLine);
327
+ }
328
+ return {
329
+ activeListingsLow, activeListingsHigh, comparableSalesLow, comparableSalesHigh,
330
+ subject, comparables, priorSaleAnalysis, summaryOfSalesComparison,
331
+ boundingBoxes: bb,
332
+ };
333
+ }
334
+ // ── Reconciliation ────────────────────────────────────────────────────────
335
+ export function parseReconciliationSection(lines) {
336
+ const bb = {};
337
+ const indLine = findLine(lines, /^Indicated Value by Sales Comparison Approach\s*\$/i);
338
+ let indicatedValueBySalesComparison = null;
339
+ if (indLine) {
340
+ const seg = indLine.segments.find((s) => s.x > 150 && /^\d/.test(s.text.trim()));
341
+ if (seg) {
342
+ indicatedValueBySalesComparison = parseNum(seg.text);
343
+ bb.indicatedBySales = toBBox(seg, indLine);
344
+ }
345
+ }
346
+ const indAllLine = findLine(lines, /^Indicated Value by: Sales Comparison/i);
347
+ let indicatedValueByCostApproach = null, indicatedValueByIncomeApproach = null;
348
+ if (indAllLine) {
349
+ for (const seg of indAllLine.segments) {
350
+ if (/Cost Approach.*\$\s*([\d,]+)/i.test(seg.text)) {
351
+ const m = seg.text.match(/\$\s*([\d,]+)/);
352
+ if (m) {
353
+ indicatedValueByCostApproach = parseNum(m[1]);
354
+ bb.indicatedByCost = toBBox(seg, indAllLine);
355
+ }
356
+ }
357
+ if (/Income Approach.*\$\s*([\d,]+)/i.test(seg.text)) {
358
+ const m = seg.text.match(/\$\s*([\d,]+)/);
359
+ if (m)
360
+ indicatedValueByIncomeApproach = parseNum(m[1]);
361
+ }
362
+ }
363
+ // Also get sales comparison from this line
364
+ if (!indicatedValueBySalesComparison) {
365
+ const salesSeg = indAllLine.segments.find((s) => s.x > 180 && s.x < 255 && /^\d/.test(s.text.trim()));
366
+ if (salesSeg)
367
+ indicatedValueBySalesComparison = parseNum(salesSeg.text);
368
+ }
369
+ }
370
+ // Reconciliation comments
371
+ const recLine = findLine(lines, /ALL WEIGHT GIVEN|reconciliation|weight.*given/i);
372
+ let reconciliationComments = "";
373
+ if (recLine) {
374
+ const idx = lines.indexOf(recLine);
375
+ const parts = [recLine.fullText.trim()];
376
+ for (let i = idx + 1; i < lines.length; i++) {
377
+ if (/^This appraisal is made|^Based on a complete/i.test(lines[i].fullText))
378
+ break;
379
+ const t = lines[i].fullText.trim();
380
+ if (t)
381
+ parts.push(t);
382
+ }
383
+ reconciliationComments = parts.join(" ").trim();
384
+ bb.reconciliationComments = toBBox(recLine.segments[0], recLine);
385
+ }
386
+ // Appraisal basis
387
+ const basisLine = findLine(lines, /This appraisal is made/i);
388
+ let appraisalBasis = "";
389
+ if (basisLine) {
390
+ const idx = lines.indexOf(basisLine);
391
+ const parts = [basisLine.fullText.trim()];
392
+ for (let i = idx + 1; i < lines.length; i++) {
393
+ if (/^Based on a complete|^\$/i.test(lines[i].fullText))
394
+ break;
395
+ parts.push(lines[i].fullText.trim());
396
+ }
397
+ appraisalBasis = parts.join(" ").trim();
398
+ }
399
+ // Final value and effective date
400
+ const valueLine = findLine(lines, /^\$\s*[\d,]+\s*,\s*as of/i);
401
+ let finalValue = null, effectiveDate = "";
402
+ if (valueLine) {
403
+ for (const seg of valueLine.segments) {
404
+ if (/^\d[\d,]*$/.test(seg.text.trim())) {
405
+ finalValue = parseNum(seg.text);
406
+ bb.finalValue = toBBox(seg, valueLine);
407
+ }
408
+ if (/^\d{2}\/\d{2}\/\d{4}$/.test(seg.text.trim())) {
409
+ effectiveDate = seg.text.trim();
410
+ bb.effectiveDate = toBBox(seg, valueLine);
411
+ }
412
+ }
413
+ }
414
+ return { indicatedValueBySalesComparison, indicatedValueByCostApproach, indicatedValueByIncomeApproach, reconciliationComments, appraisalBasis, finalValue, effectiveDate, boundingBoxes: bb };
415
+ }
416
+ // ── Cost Approach (Page 3) ────────────────────────────────────────────────
417
+ export function parseCostApproachSection(lines) {
418
+ const bb = {};
419
+ function findCostVal(pattern) {
420
+ for (const l of lines) {
421
+ if (!pattern.test(l.fullText))
422
+ continue;
423
+ // Dollar value is usually at the far right (x > 500)
424
+ const valSeg = l.segments.filter((s) => s.x > 480);
425
+ for (const seg of valSeg) {
426
+ const n = parseNum(seg.text);
427
+ if (n !== null)
428
+ return n;
429
+ }
430
+ }
431
+ return null;
432
+ }
433
+ const siteValueLine = findLine(lines, /support for the opinion of site value/i);
434
+ let siteValueSupport = "";
435
+ if (siteValueLine) {
436
+ const valueSeg = siteValueLine.segments.find((s) => s.x > 250);
437
+ if (valueSeg)
438
+ siteValueSupport = valueSeg.text.trim();
439
+ // Continue on next lines
440
+ const idx = lines.indexOf(siteValueLine);
441
+ const parts = siteValueSupport ? [siteValueSupport] : [];
442
+ for (let i = idx + 1; i < lines.length; i++) {
443
+ if (/^ESTIMATED|^OPINION OF SITE/i.test(lines[i].fullText))
444
+ break;
445
+ parts.push(lines[i].fullText.trim());
446
+ }
447
+ siteValueSupport = parts.join(" ").trim();
448
+ }
449
+ const siteValue = findCostVal(/OPINION OF SITE VALUE/i);
450
+ // Dwelling cost
451
+ const dwellingLine = findLine(lines, /^DWELLING/i);
452
+ let dwellingSqft = null, dwellingCostPerSqft = null, dwellingCost = null;
453
+ if (dwellingLine) {
454
+ const sqftMatch = dwellingLine.fullText.match(/([\d,]+)\s*Sq\.?\s*Ft/i);
455
+ if (sqftMatch)
456
+ dwellingSqft = parseNum(sqftMatch[1]);
457
+ const costMatch = dwellingLine.fullText.match(/@\s*\$?\s*([\d,.]+)/i) || dwellingLine.fullText.match(/(\d+\.?\d*)\s*=\s*\$/);
458
+ if (costMatch)
459
+ dwellingCostPerSqft = parseNum(costMatch[1]);
460
+ const valSegs = dwellingLine.segments.filter((s) => s.x > 480);
461
+ for (const seg of valSegs) {
462
+ const n = parseNum(seg.text);
463
+ if (n !== null) {
464
+ dwellingCost = n;
465
+ bb.dwellingCost = toBBox(seg, dwellingLine);
466
+ }
467
+ }
468
+ }
469
+ const amenitiesCost = findCostVal(/AMENITIES/i);
470
+ // Garage/Carport
471
+ const garageLine = findLine(lines, /^Garage\/Carport/i);
472
+ let garageCarportSqft = null, garageCarportCostPerSqft = null, garageCarportCost = null;
473
+ if (garageLine) {
474
+ const sqftMatch = garageLine.fullText.match(/([\d,]+)\s*Sq\.?\s*Ft/i);
475
+ if (sqftMatch)
476
+ garageCarportSqft = parseNum(sqftMatch[1]);
477
+ const costMatch = garageLine.fullText.match(/@\s*\$?\s*([\d,.]+)/i) || garageLine.fullText.match(/(\d+\.?\d*)\s*=\s*\$/);
478
+ if (costMatch)
479
+ garageCarportCostPerSqft = parseNum(costMatch[1]);
480
+ const valSegs = garageLine.segments.filter((s) => s.x > 480);
481
+ for (const seg of valSegs) {
482
+ const n = parseNum(seg.text);
483
+ if (n !== null)
484
+ garageCarportCost = n;
485
+ }
486
+ }
487
+ const totalCostNew = findCostVal(/Total Estimate of Cost-?New/i);
488
+ const depreciation = findCostVal(/^Depreciation\b/i);
489
+ const depreciatedCostOfImprovements = findCostVal(/Depreciated Cost of Improvements/i);
490
+ const asIsValueOfSiteImprovements = findCostVal(/As-?is.*Value of Site Improvements/i);
491
+ const indicatedValueByCostApproach = findCostVal(/INDICATED VALUE BY COST APPROACH/i);
492
+ const erlLine = findLine(lines, /Estimated Remaining Economic Life/i);
493
+ let estimatedRemainingEconomicLife = null;
494
+ if (erlLine) {
495
+ const m = erlLine.fullText.match(/(\d+)\s*Years/i);
496
+ if (m)
497
+ estimatedRemainingEconomicLife = parseNum(m[1]);
498
+ }
499
+ return {
500
+ siteValueSupport, siteValue, dwellingSqft, dwellingCostPerSqft, dwellingCost,
501
+ amenitiesCost, garageCarportSqft, garageCarportCostPerSqft, garageCarportCost,
502
+ totalCostNew, depreciation, depreciatedCostOfImprovements, asIsValueOfSiteImprovements,
503
+ indicatedValueByCostApproach, estimatedRemainingEconomicLife, boundingBoxes: bb,
504
+ };
505
+ }
@@ -0,0 +1,5 @@
1
+ import type { TextLine } from "@parseo/shared";
2
+ import type { Form1004MCReport } from "./types.js";
3
+ export declare function parseForm1004MC(buffer: Buffer): Promise<Form1004MCReport>;
4
+ export declare function parseForm1004MCFromLines(lines: TextLine[], buffer: Buffer, pageOffset?: number): Promise<Form1004MCReport>;
5
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/form-1004mc/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAA4B,MAAM,gBAAgB,CAAC;AACzE,OAAO,KAAK,EACV,gBAAgB,EASjB,MAAM,YAAY,CAAC;AAKpB,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAG/E;AAED,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,MAAU,GACrB,OAAO,CAAC,gBAAgB,CAAC,CAgJ3B"}