@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.
- package/README.md +35 -0
- package/dist/form-1004mc/extract-checkboxes.d.ts +43 -0
- package/dist/form-1004mc/extract-checkboxes.d.ts.map +1 -0
- package/dist/form-1004mc/extract-checkboxes.js +145 -0
- package/dist/form-1004mc/index.d.ts +3 -0
- package/dist/form-1004mc/index.d.ts.map +1 -0
- package/dist/form-1004mc/index.js +1 -0
- package/dist/form-1004mc/parse-page1.d.ts +8 -0
- package/dist/form-1004mc/parse-page1.d.ts.map +1 -0
- package/dist/form-1004mc/parse-page1.js +760 -0
- package/dist/form-1004mc/parse-sales.d.ts +6 -0
- package/dist/form-1004mc/parse-sales.d.ts.map +1 -0
- package/dist/form-1004mc/parse-sales.js +505 -0
- package/dist/form-1004mc/parser.d.ts +5 -0
- package/dist/form-1004mc/parser.d.ts.map +1 -0
- package/dist/form-1004mc/parser.js +437 -0
- package/dist/form-1004mc/types.d.ts +302 -0
- package/dist/form-1004mc/types.d.ts.map +1 -0
- package/dist/form-1004mc/types.js +1 -0
- package/dist/form-1073/index.d.ts +3 -0
- package/dist/form-1073/index.d.ts.map +1 -0
- package/dist/form-1073/index.js +1 -0
- package/dist/form-1073/parse-page1.d.ts +8 -0
- package/dist/form-1073/parse-page1.d.ts.map +1 -0
- package/dist/form-1073/parse-page1.js +704 -0
- package/dist/form-1073/parse-page2.d.ts +6 -0
- package/dist/form-1073/parse-page2.d.ts.map +1 -0
- package/dist/form-1073/parse-page2.js +438 -0
- package/dist/form-1073/parse-sales.d.ts +7 -0
- package/dist/form-1073/parse-sales.d.ts.map +1 -0
- package/dist/form-1073/parse-sales.js +477 -0
- package/dist/form-1073/parser.d.ts +5 -0
- package/dist/form-1073/parser.d.ts.map +1 -0
- package/dist/form-1073/parser.js +102 -0
- package/dist/form-1073/types.d.ts +300 -0
- package/dist/form-1073/types.d.ts.map +1 -0
- package/dist/form-1073/types.js +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/richer-values/index.d.ts +3 -0
- package/dist/richer-values/index.d.ts.map +1 -0
- package/dist/richer-values/index.js +1 -0
- package/dist/richer-values/parser.d.ts +5 -0
- package/dist/richer-values/parser.d.ts.map +1 -0
- package/dist/richer-values/parser.js +1067 -0
- package/dist/richer-values/types.d.ts +225 -0
- package/dist/richer-values/types.d.ts.map +1 -0
- package/dist/richer-values/types.js +1 -0
- package/package.json +24 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TextLine } from "@parseo/shared";
|
|
2
|
+
import type { ProjectAnalysisSection, UnitDescriptionSection, PriorSaleHistorySection } from "./types.js";
|
|
3
|
+
export declare function parseProjectAnalysisSection(lines: TextLine[]): ProjectAnalysisSection;
|
|
4
|
+
export declare function parseUnitDescriptionSection(lines: TextLine[]): UnitDescriptionSection;
|
|
5
|
+
export declare function parsePriorSaleHistorySection(lines: TextLine[]): PriorSaleHistorySection;
|
|
6
|
+
//# sourceMappingURL=parse-page2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-page2.d.ts","sourceRoot":"","sources":["../../src/form-1073/parse-page2.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EAExB,MAAM,YAAY,CAAC;AAqCpB,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,sBAAsB,CAmHrF;AAID,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,sBAAsB,CA8KrF;AAID,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,uBAAuB,CAgDvF"}
|
|
@@ -0,0 +1,438 @@
|
|
|
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)
|
|
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
|
+
function extractAfterLabel(seg, label) {
|
|
17
|
+
return seg.text.replace(label, "").trim();
|
|
18
|
+
}
|
|
19
|
+
function findLine(lines, pattern) {
|
|
20
|
+
return lines.find((l) => pattern.test(l.fullText));
|
|
21
|
+
}
|
|
22
|
+
function collectText(lines, startIdx, endPattern) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
25
|
+
if (endPattern.test(lines[i].fullText))
|
|
26
|
+
break;
|
|
27
|
+
const t = lines[i].fullText.trim();
|
|
28
|
+
if (t)
|
|
29
|
+
parts.push(t);
|
|
30
|
+
}
|
|
31
|
+
return parts.join(" ").trim();
|
|
32
|
+
}
|
|
33
|
+
// ── Project Analysis (Page 2 top) ────────────────────────────────────────
|
|
34
|
+
export function parseProjectAnalysisSection(lines) {
|
|
35
|
+
const bb = {};
|
|
36
|
+
// Condition and quality
|
|
37
|
+
const condLine = findLine(lines, /condition of the project and quality/i);
|
|
38
|
+
let conditionAndQuality = "";
|
|
39
|
+
if (condLine) {
|
|
40
|
+
const idx = lines.indexOf(condLine);
|
|
41
|
+
const valueSeg = condLine.segments.find((s) => s.x > 300);
|
|
42
|
+
const parts = [];
|
|
43
|
+
if (valueSeg)
|
|
44
|
+
parts.push(valueSeg.text.trim());
|
|
45
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
46
|
+
if (/^Describe the common elements/i.test(lines[i].fullText))
|
|
47
|
+
break;
|
|
48
|
+
const t = lines[i].fullText.trim();
|
|
49
|
+
if (t)
|
|
50
|
+
parts.push(t);
|
|
51
|
+
}
|
|
52
|
+
conditionAndQuality = parts.join(" ").trim();
|
|
53
|
+
bb.conditionAndQuality = toBBox(condLine.segments[0], condLine);
|
|
54
|
+
}
|
|
55
|
+
// Common elements
|
|
56
|
+
const commLine = findLine(lines, /common elements and recreational/i);
|
|
57
|
+
let commonElements = "";
|
|
58
|
+
if (commLine) {
|
|
59
|
+
const valueSeg = commLine.segments.find((s) => s.x > 280);
|
|
60
|
+
if (valueSeg) {
|
|
61
|
+
commonElements = valueSeg.text.trim();
|
|
62
|
+
bb.commonElements = toBBox(valueSeg, commLine);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Common elements leased
|
|
66
|
+
const leasedLine = findLine(lines, /common elements leased/i);
|
|
67
|
+
const commonElementsLeased = leasedLine?.fullText.includes("Yes") ? "Yes" : leasedLine?.fullText.includes("No") ? "No" : "";
|
|
68
|
+
// Ground rent
|
|
69
|
+
const groundLine = findLine(lines, /subject to a ground rent/i);
|
|
70
|
+
const groundRent = groundLine?.fullText.includes("Yes") ? "Yes" : groundLine?.fullText.includes("No") ? "No" : "";
|
|
71
|
+
// Parking adequacy
|
|
72
|
+
const parkLine = findLine(lines, /parking facilities adequate/i);
|
|
73
|
+
const parkingAdequacy = parkLine?.fullText.includes("Yes") ? "Yes" : parkLine?.fullText.includes("No") ? "No" : "";
|
|
74
|
+
// Budget analysis
|
|
75
|
+
const budgetLine = findLine(lines, /analyze the condominium project budget/i);
|
|
76
|
+
let budgetAnalysis = "";
|
|
77
|
+
if (budgetLine) {
|
|
78
|
+
const idx = lines.indexOf(budgetLine);
|
|
79
|
+
const parts = [];
|
|
80
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
81
|
+
if (/^Are there any other fees/i.test(lines[i].fullText))
|
|
82
|
+
break;
|
|
83
|
+
const t = lines[i].fullText.trim();
|
|
84
|
+
if (t)
|
|
85
|
+
parts.push(t);
|
|
86
|
+
}
|
|
87
|
+
budgetAnalysis = parts.join(" ").trim();
|
|
88
|
+
}
|
|
89
|
+
// Other fees
|
|
90
|
+
const feeLine = findLine(lines, /other fees.*for the use of/i);
|
|
91
|
+
const otherFees = feeLine?.fullText.includes("Yes") ? "Yes" : feeLine?.fullText.includes("No") ? "No" : "";
|
|
92
|
+
// Unit charge comparison
|
|
93
|
+
const compLine = findLine(lines, /subject unit charge appears/i);
|
|
94
|
+
let unitChargeComparison = "";
|
|
95
|
+
if (compLine) {
|
|
96
|
+
if (/\bHigh\b/.test(compLine.fullText))
|
|
97
|
+
unitChargeComparison = "High";
|
|
98
|
+
else if (/\bAverage\b/.test(compLine.fullText))
|
|
99
|
+
unitChargeComparison = "Average";
|
|
100
|
+
else if (/\bLow\b/.test(compLine.fullText))
|
|
101
|
+
unitChargeComparison = "Low";
|
|
102
|
+
}
|
|
103
|
+
// Special characteristics
|
|
104
|
+
const specLine = findLine(lines, /special or unusual characteristics/i);
|
|
105
|
+
let specialCharacteristics = "";
|
|
106
|
+
if (specLine) {
|
|
107
|
+
const idx = lines.indexOf(specLine);
|
|
108
|
+
const parts = [];
|
|
109
|
+
// Check next line(s)
|
|
110
|
+
if (lines[idx + 1]) {
|
|
111
|
+
const valueSeg = lines[idx + 1].segments.find((s) => s.x > 280);
|
|
112
|
+
if (valueSeg)
|
|
113
|
+
parts.push(valueSeg.text.trim());
|
|
114
|
+
}
|
|
115
|
+
for (let i = idx + 2; i < lines.length; i++) {
|
|
116
|
+
if (/^Unit Charge/i.test(lines[i].fullText))
|
|
117
|
+
break;
|
|
118
|
+
const t = lines[i].fullText.trim();
|
|
119
|
+
if (t)
|
|
120
|
+
parts.push(t);
|
|
121
|
+
}
|
|
122
|
+
specialCharacteristics = parts.join(" ").trim();
|
|
123
|
+
}
|
|
124
|
+
// Unit charge
|
|
125
|
+
const chargeLine = findLine(lines, /^Unit Charge \$/i);
|
|
126
|
+
let unitChargeMonthly = null, unitChargeAnnual = null, assessmentPerSqft = null;
|
|
127
|
+
if (chargeLine) {
|
|
128
|
+
for (const seg of chargeLine.segments) {
|
|
129
|
+
const t = seg.text.trim();
|
|
130
|
+
if (/^Unit Charge \$\s/i.test(t)) {
|
|
131
|
+
unitChargeMonthly = parseNum(extractAfterLabel(seg, /^Unit Charge \$\s*/i));
|
|
132
|
+
bb.unitChargeMonthly = toBBox(seg, chargeLine);
|
|
133
|
+
}
|
|
134
|
+
const annualMatch = t.match(/\$\s*([\d,.]+)\s*per year/i);
|
|
135
|
+
if (annualMatch)
|
|
136
|
+
unitChargeAnnual = parseNum(annualMatch[1]);
|
|
137
|
+
const sqftMatch = t.match(/=\s*\$\s*([\d,.]+)/i);
|
|
138
|
+
if (sqftMatch)
|
|
139
|
+
assessmentPerSqft = parseNum(sqftMatch[1]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Utilities included
|
|
143
|
+
const utilLine = findLine(lines, /Utilities included in the unit/i);
|
|
144
|
+
let utilitiesIncluded = "";
|
|
145
|
+
if (utilLine) {
|
|
146
|
+
// All labels after "None" or specific utility names
|
|
147
|
+
const segs = utilLine.segments.filter((s) => s.x > 200);
|
|
148
|
+
utilitiesIncluded = segs.map((s) => s.text.trim()).join(", ");
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
conditionAndQuality, commonElements, commonElementsLeased, groundRent,
|
|
152
|
+
parkingAdequacy, budgetAnalysis, otherFees, unitChargeComparison, specialCharacteristics,
|
|
153
|
+
unitChargeMonthly, unitChargeAnnual, assessmentPerSqft, utilitiesIncluded,
|
|
154
|
+
boundingBoxes: bb,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// ── Unit Description (Page 2) ────────────────────────────────────────────
|
|
158
|
+
export function parseUnitDescriptionSection(lines) {
|
|
159
|
+
const bb = {};
|
|
160
|
+
const floorLine = findLine(lines, /^Floor #/i);
|
|
161
|
+
let floorNumber = "";
|
|
162
|
+
if (floorLine) {
|
|
163
|
+
const seg = floorLine.segments.find((s) => /^Floor #/i.test(s.text));
|
|
164
|
+
if (seg)
|
|
165
|
+
floorNumber = extractAfterLabel(seg, /^Floor #\s*/i);
|
|
166
|
+
}
|
|
167
|
+
const levelsLine = findLine(lines, /^# of Levels/i);
|
|
168
|
+
let numberOfLevels = null;
|
|
169
|
+
if (levelsLine) {
|
|
170
|
+
const seg = levelsLine.segments.find((s) => /^# of Levels/i.test(s.text));
|
|
171
|
+
if (seg)
|
|
172
|
+
numberOfLevels = parseNum(extractAfterLabel(seg, /^# of Levels\s+/i));
|
|
173
|
+
}
|
|
174
|
+
const heatLine = findLine(lines, /^Heating Type/i);
|
|
175
|
+
let heatingType = "", heatingFuel = "";
|
|
176
|
+
if (heatLine) {
|
|
177
|
+
for (const seg of heatLine.segments) {
|
|
178
|
+
const t = seg.text.trim();
|
|
179
|
+
if (/^Heating Type\s/i.test(t))
|
|
180
|
+
heatingType = extractAfterLabel(seg, /^Heating Type\s+/i);
|
|
181
|
+
else if (/^Fuel\s/i.test(t))
|
|
182
|
+
heatingFuel = extractAfterLabel(seg, /^Fuel\s+/i);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const acLine = findLine(lines, /^Central AC/i);
|
|
186
|
+
let centralAC = "";
|
|
187
|
+
if (acLine) {
|
|
188
|
+
const seg = acLine.segments.find((s) => /^Central AC/i.test(s.text) || /^Individual AC/i.test(s.text));
|
|
189
|
+
if (seg)
|
|
190
|
+
centralAC = /Central AC/i.test(seg.text) ? "Central" : "Individual";
|
|
191
|
+
}
|
|
192
|
+
// Interior materials
|
|
193
|
+
function findMaterial(label) {
|
|
194
|
+
for (const l of lines) {
|
|
195
|
+
if (l.y < 415 || l.y > 500)
|
|
196
|
+
continue;
|
|
197
|
+
for (const seg of l.segments) {
|
|
198
|
+
if (label.test(seg.text)) {
|
|
199
|
+
const val = seg.text.replace(label, "").trim();
|
|
200
|
+
if (val)
|
|
201
|
+
return val;
|
|
202
|
+
const next = l.segments[l.segments.indexOf(seg) + 1];
|
|
203
|
+
if (next)
|
|
204
|
+
return next.text.trim();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return "";
|
|
209
|
+
}
|
|
210
|
+
const floors = findMaterial(/^Floors\s+/i);
|
|
211
|
+
const walls = findMaterial(/^Walls\s+/i);
|
|
212
|
+
const trimFinish = findMaterial(/^Trim\/Finish\s+/i);
|
|
213
|
+
const bathWainscot = findMaterial(/^Bath Wainscot\s+/i);
|
|
214
|
+
const doors = findMaterial(/^Doors\s+/i);
|
|
215
|
+
// Amenities
|
|
216
|
+
let fireplaces = null, deckPatio = "", porchBalcony = "";
|
|
217
|
+
for (const l of lines) {
|
|
218
|
+
if (l.y < 415 || l.y > 500)
|
|
219
|
+
continue;
|
|
220
|
+
for (const seg of l.segments) {
|
|
221
|
+
const t = seg.text.trim();
|
|
222
|
+
if (/^Fireplace\(s\) #/i.test(t))
|
|
223
|
+
fireplaces = parseNum(extractAfterLabel(seg, /^Fireplace\(s\) #\s*/i));
|
|
224
|
+
else if (/^Deck\/Patio/i.test(t))
|
|
225
|
+
deckPatio = extractAfterLabel(seg, /^Deck\/Patio\s*/i);
|
|
226
|
+
else if (/^Porch\/Balcony/i.test(t))
|
|
227
|
+
porchBalcony = extractAfterLabel(seg, /^Porch\/Balcony\s*/i);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Appliances
|
|
231
|
+
let refrigerator = "", rangeOven = "", disposal = "", dishwasher = "", microwave = "", washerDryer = "";
|
|
232
|
+
for (const l of lines) {
|
|
233
|
+
if (l.y < 415 || l.y > 500)
|
|
234
|
+
continue;
|
|
235
|
+
for (const seg of l.segments) {
|
|
236
|
+
const t = seg.text.trim();
|
|
237
|
+
if (/^Refrigerator$/i.test(t))
|
|
238
|
+
refrigerator = "Yes";
|
|
239
|
+
if (/^Range\/Oven$/i.test(t))
|
|
240
|
+
rangeOven = "Yes";
|
|
241
|
+
if (/^Disp$/i.test(t) || /^Disposal$/i.test(t))
|
|
242
|
+
disposal = "Yes";
|
|
243
|
+
if (/^Dishwasher$/i.test(t))
|
|
244
|
+
dishwasher = "Yes";
|
|
245
|
+
if (/^Microwave$/i.test(t))
|
|
246
|
+
microwave = "Yes";
|
|
247
|
+
if (/^Washer\/Dryer$/i.test(t))
|
|
248
|
+
washerDryer = "Yes";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Car storage
|
|
252
|
+
let carStorageType = "", carStorageCount = null, carStorageOwnership = "";
|
|
253
|
+
for (const l of lines) {
|
|
254
|
+
if (l.y < 415 || l.y > 500)
|
|
255
|
+
continue;
|
|
256
|
+
for (const seg of l.segments) {
|
|
257
|
+
const t = seg.text.trim();
|
|
258
|
+
if (/^Garage$/i.test(t))
|
|
259
|
+
carStorageType = "Garage";
|
|
260
|
+
if (/^# of Cars\s/i.test(t))
|
|
261
|
+
carStorageCount = parseNum(extractAfterLabel(seg, /^# of Cars\s+/i));
|
|
262
|
+
if (/^Assigned$/i.test(t))
|
|
263
|
+
carStorageOwnership = "Assigned";
|
|
264
|
+
if (/^Owned$/i.test(t))
|
|
265
|
+
carStorageOwnership = "Owned";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Room count
|
|
269
|
+
const roomLine = findLine(lines, /^Finished area above grade/i);
|
|
270
|
+
let roomCount = null, bedrooms = null, baths = null, grossLivingArea = null;
|
|
271
|
+
if (roomLine) {
|
|
272
|
+
for (const seg of roomLine.segments) {
|
|
273
|
+
const t = seg.text.trim();
|
|
274
|
+
const roomMatch = t.match(/^(\d+)\s*Rooms?/i);
|
|
275
|
+
if (roomMatch) {
|
|
276
|
+
roomCount = parseNum(roomMatch[1]);
|
|
277
|
+
bb.roomCount = toBBox(seg, roomLine);
|
|
278
|
+
}
|
|
279
|
+
const bedMatch = t.match(/^(\d+)\s*Bedrooms?/i);
|
|
280
|
+
if (bedMatch) {
|
|
281
|
+
bedrooms = parseNum(bedMatch[1]);
|
|
282
|
+
bb.bedrooms = toBBox(seg, roomLine);
|
|
283
|
+
}
|
|
284
|
+
const bathMatch = t.match(/^([\d.]+)\s*Bath/i);
|
|
285
|
+
if (bathMatch) {
|
|
286
|
+
baths = parseNum(bathMatch[1]);
|
|
287
|
+
bb.baths = toBBox(seg, roomLine);
|
|
288
|
+
}
|
|
289
|
+
const sqftMatch = t.match(/^([\d,]+)\s*Square Feet/i);
|
|
290
|
+
if (sqftMatch) {
|
|
291
|
+
grossLivingArea = parseNum(sqftMatch[1]);
|
|
292
|
+
bb.grossLivingArea = toBBox(seg, roomLine);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const meterLine = findLine(lines, /separately metered/i);
|
|
297
|
+
const separatelyMetered = meterLine?.fullText.includes("Yes") ? "Yes" : meterLine?.fullText.includes("No") ? "No" : "";
|
|
298
|
+
const featLine = findLine(lines, /^Additional features/i);
|
|
299
|
+
let additionalFeatures = "";
|
|
300
|
+
if (featLine) {
|
|
301
|
+
const valueSeg = featLine.segments.find((s) => s.x > 180);
|
|
302
|
+
if (valueSeg)
|
|
303
|
+
additionalFeatures = valueSeg.text.trim();
|
|
304
|
+
}
|
|
305
|
+
const condLine = findLine(lines, /^Describe the condition of the property/i);
|
|
306
|
+
let conditionDescription = "";
|
|
307
|
+
if (condLine) {
|
|
308
|
+
const idx = lines.indexOf(condLine);
|
|
309
|
+
const valueSeg = condLine.segments.find((s) => s.x > 350);
|
|
310
|
+
const parts = [];
|
|
311
|
+
if (valueSeg)
|
|
312
|
+
parts.push(valueSeg.text.trim());
|
|
313
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
314
|
+
if (/^Are there any physical/i.test(lines[i].fullText) || /^UNIT DESCRIPTION$/i.test(lines[i].fullText.trim()))
|
|
315
|
+
break;
|
|
316
|
+
const t = lines[i].fullText.trim();
|
|
317
|
+
if (t)
|
|
318
|
+
parts.push(t);
|
|
319
|
+
}
|
|
320
|
+
conditionDescription = parts.join(" ").trim();
|
|
321
|
+
}
|
|
322
|
+
const defLine = findLine(lines, /physical deficiencies or adverse conditions/i);
|
|
323
|
+
let physicalDeficiencies = "";
|
|
324
|
+
if (defLine) {
|
|
325
|
+
const idx = lines.indexOf(defLine);
|
|
326
|
+
const parts = [];
|
|
327
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
328
|
+
if (/^Does the property generally conform/i.test(lines[i].fullText))
|
|
329
|
+
break;
|
|
330
|
+
const t = lines[i].fullText.trim();
|
|
331
|
+
if (t)
|
|
332
|
+
parts.push(t);
|
|
333
|
+
}
|
|
334
|
+
physicalDeficiencies = parts.join(" ").trim();
|
|
335
|
+
}
|
|
336
|
+
const confLine = findLine(lines, /Does the property generally conform/i);
|
|
337
|
+
let conformity = "";
|
|
338
|
+
if (confLine) {
|
|
339
|
+
const idx = lines.indexOf(confLine);
|
|
340
|
+
const parts = [];
|
|
341
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
342
|
+
if (/^I\s+(did|did not)\s+research/i.test(lines[i].fullText) || /^Freddie Mac Form/i.test(lines[i].fullText))
|
|
343
|
+
break;
|
|
344
|
+
const t = lines[i].fullText.trim();
|
|
345
|
+
if (t)
|
|
346
|
+
parts.push(t);
|
|
347
|
+
}
|
|
348
|
+
conformity = parts.join(" ").trim();
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
floorNumber, numberOfLevels, heatingType, heatingFuel, centralAC,
|
|
352
|
+
floors, walls, trimFinish, bathWainscot, doors,
|
|
353
|
+
fireplaces, deckPatio, porchBalcony,
|
|
354
|
+
refrigerator, rangeOven, disposal, dishwasher, microwave, washerDryer,
|
|
355
|
+
carStorageType, carStorageCount, carStorageOwnership,
|
|
356
|
+
roomCount, bedrooms, baths, grossLivingArea,
|
|
357
|
+
separatelyMetered, additionalFeatures, conditionDescription, physicalDeficiencies, conformity,
|
|
358
|
+
boundingBoxes: bb,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// ── Prior Sale History (Page 2 bottom) ───────────────────────────────────
|
|
362
|
+
export function parsePriorSaleHistorySection(lines) {
|
|
363
|
+
const bb = {};
|
|
364
|
+
const researchLine = findLine(lines, /did.*research the sale or transfer/i);
|
|
365
|
+
const researchPerformed = researchLine?.fullText.includes("did not") ? "did not" : "did";
|
|
366
|
+
const subjRevealLine = findLine(lines, /My research.*reveal any prior sales.*subject property/i);
|
|
367
|
+
const subjectPriorSaleRevealed = subjRevealLine?.fullText.includes("did not") ? "did not" : "did";
|
|
368
|
+
const subjDsLine = findLine(lines, /^Data source\(s\)\s+Public Records/i);
|
|
369
|
+
const subjectDataSources = subjDsLine?.fullText.replace(/^Data source\(s\)\s*/i, "").trim() ?? "";
|
|
370
|
+
const compRevealLine = findLine(lines, /My research.*reveal any prior sales.*comparable/i);
|
|
371
|
+
const comparablePriorSaleRevealed = compRevealLine?.fullText.includes("did not") ? "did not" : "did";
|
|
372
|
+
// Look for the second Data source(s) line
|
|
373
|
+
const allDsLines = lines.filter((l) => /^Data source\(s\)/i.test(l.fullText));
|
|
374
|
+
const comparableDataSources = allDsLines.length > 1 ? allDsLines[1].fullText.replace(/^Data source\(s\)\s*/i, "").trim() : "";
|
|
375
|
+
// Prior sale table: ITEM / SUBJECT / COMP1 / COMP2 / COMP3
|
|
376
|
+
const headerLine = findLine(lines, /^ITEM\s+SUBJECT\s+COMPARABLE SALE/i);
|
|
377
|
+
const subject = parsePriorSaleColumn(lines, headerLine, 0);
|
|
378
|
+
const comparables = [];
|
|
379
|
+
for (let i = 1; i <= 3; i++) {
|
|
380
|
+
comparables.push(parsePriorSaleColumn(lines, headerLine, i));
|
|
381
|
+
}
|
|
382
|
+
// Analysis text
|
|
383
|
+
const analysisLine = findLine(lines, /^Analysis of prior sale or transfer/i);
|
|
384
|
+
let analysis = "";
|
|
385
|
+
if (analysisLine) {
|
|
386
|
+
const idx = lines.indexOf(analysisLine);
|
|
387
|
+
const valueSeg = analysisLine.segments.find((s) => s.x > 280);
|
|
388
|
+
const parts = [];
|
|
389
|
+
if (valueSeg)
|
|
390
|
+
parts.push(valueSeg.text.trim());
|
|
391
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
392
|
+
if (/^Freddie Mac Form/i.test(lines[i].fullText))
|
|
393
|
+
break;
|
|
394
|
+
const t = lines[i].fullText.trim();
|
|
395
|
+
if (t)
|
|
396
|
+
parts.push(t);
|
|
397
|
+
}
|
|
398
|
+
analysis = parts.join(" ").trim();
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
researchPerformed, subjectPriorSaleRevealed, subjectDataSources,
|
|
402
|
+
comparablePriorSaleRevealed, comparableDataSources,
|
|
403
|
+
subject, comparables, analysis, boundingBoxes: bb,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function parsePriorSaleColumn(lines, headerLine, colIndex) {
|
|
407
|
+
const bb = {};
|
|
408
|
+
if (!headerLine)
|
|
409
|
+
return { dateOfPriorSale: "", priceOfPriorSale: null, dataSources: "", effectiveDateOfDataSources: "", boundingBoxes: bb };
|
|
410
|
+
// Column x boundaries based on header segments
|
|
411
|
+
const colRanges = [
|
|
412
|
+
{ min: 80, max: 180 }, // Subject
|
|
413
|
+
{ min: 180, max: 320 }, // Comp 1
|
|
414
|
+
{ min: 320, max: 450 }, // Comp 2
|
|
415
|
+
{ min: 450, max: 580 }, // Comp 3
|
|
416
|
+
];
|
|
417
|
+
const col = colRanges[colIndex] ?? colRanges[0];
|
|
418
|
+
function getColValue(line) {
|
|
419
|
+
if (!line)
|
|
420
|
+
return "";
|
|
421
|
+
return line.segments
|
|
422
|
+
.filter((s) => s.x >= col.min && s.x < col.max)
|
|
423
|
+
.map((s) => s.text.trim())
|
|
424
|
+
.join(" ")
|
|
425
|
+
.trim();
|
|
426
|
+
}
|
|
427
|
+
const headerIdx = lines.indexOf(headerLine);
|
|
428
|
+
const dateLine = lines[headerIdx + 1];
|
|
429
|
+
const priceLine = lines[headerIdx + 2];
|
|
430
|
+
const dsLine = lines[headerIdx + 3];
|
|
431
|
+
const effLine = lines[headerIdx + 4];
|
|
432
|
+
const dateOfPriorSale = getColValue(dateLine);
|
|
433
|
+
const priceText = getColValue(priceLine);
|
|
434
|
+
const priceOfPriorSale = priceText ? parseCurrency(priceText) : null;
|
|
435
|
+
const dataSources = getColValue(dsLine);
|
|
436
|
+
const effectiveDateOfDataSources = getColValue(effLine);
|
|
437
|
+
return { dateOfPriorSale, priceOfPriorSale, dataSources, effectiveDateOfDataSources, boundingBoxes: bb };
|
|
438
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TextLine } from "@parseo/shared";
|
|
2
|
+
import type { SalesComparisonSection, ReconciliationSection, AppraiserInfo, LenderClientInfo } 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 parseAppraiserInfo(lines: TextLine[], supervisory: boolean): AppraiserInfo | null;
|
|
6
|
+
export declare function parseLenderClientInfo(lines: TextLine[]): LenderClientInfo;
|
|
7
|
+
//# sourceMappingURL=parse-sales.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-sales.d.ts","sourceRoot":"","sources":["../../src/form-1073/parse-sales.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EACV,sBAAsB,EAGtB,qBAAqB,EACrB,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AA8NpB,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,QAAQ,EAAE,EACjB,YAAY,GAAE,MAAU,GACvB,sBAAsB,CAoIxB;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,qBAAqB,CA4DnF;AAID,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CA2ChG;AAID,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,gBAAgB,CAgCzE"}
|