@mountainpass/addressr 2.1.5 → 2.3.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.
|
@@ -111,6 +111,14 @@ async function initIndex(esClient, clear, synonyms) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
|
+
// ADR 026: range-number address expansion. Multi-valued text field
|
|
115
|
+
// populated with one expanded address per in-range number for range-
|
|
116
|
+
// numbered G-NAF docs (span cap 20). Absent on non-range docs
|
|
117
|
+
// (asymmetric population). No .raw sub-field — never sorted on.
|
|
118
|
+
sla_range_expanded: {
|
|
119
|
+
type: 'text',
|
|
120
|
+
analyzer: 'my_analyzer'
|
|
121
|
+
},
|
|
114
122
|
confidence: {
|
|
115
123
|
type: 'integer'
|
|
116
124
|
},
|
|
@@ -30,6 +30,7 @@ var _unzipStream = _interopRequireDefault(require("unzip-stream"));
|
|
|
30
30
|
var _elasticsearch = require("../client/elasticsearch");
|
|
31
31
|
var _streamDown = _interopRequireDefault(require("../utils/stream-down"));
|
|
32
32
|
var _setLinkOptions = require("./set-link-options");
|
|
33
|
+
var _rangeExpansion = require("./range-expansion");
|
|
33
34
|
var _keyv = require("keyv");
|
|
34
35
|
var _keyvFile = require("keyv-file");
|
|
35
36
|
var _nodeCrypto = _interopRequireDefault(require("node:crypto"));
|
|
@@ -675,18 +676,34 @@ function mapAddressDetails(d, context, index, count) {
|
|
|
675
676
|
};
|
|
676
677
|
rval.mla = mapToMla(rval.structured);
|
|
677
678
|
rval.sla = mapToSla(rval.mla);
|
|
678
|
-
if (rval.structured.flat
|
|
679
|
+
if (rval.structured.flat == undefined) {
|
|
680
|
+
// Symmetric ssla so BM25 summation across sla+ssla does not favour
|
|
681
|
+
// sub-unit docs over exact street-level matches. See ADR 025 / P007.
|
|
682
|
+
rval.ssla = rval.sla;
|
|
683
|
+
} else {
|
|
679
684
|
rval.smla = mapToShortMla(rval.structured);
|
|
680
685
|
rval.ssla = mapToSla(rval.smla);
|
|
681
686
|
}
|
|
687
|
+
// ADR 026: asymmetric population of sla_range_expanded — one expanded
|
|
688
|
+
// alias per in-range number for range-numbered records up to SPAN_CAP.
|
|
689
|
+
// Non-range docs leave the field absent. expandRangeAliases returns []
|
|
690
|
+
// for span>cap or invalid inputs; in that case we keep the field absent
|
|
691
|
+
// rather than storing an empty array so OpenSearch _source stays clean.
|
|
692
|
+
if (rval.structured.number?.last?.number !== undefined) {
|
|
693
|
+
const s = rval.structured;
|
|
694
|
+
const streetType = s.street.type ? ` ${s.street.type.name}` : '';
|
|
695
|
+
const streetSuffix = s.street.suffix ? ` ${s.street.suffix.name}` : '';
|
|
696
|
+
rval.sla_range_expanded = (0, _rangeExpansion.expandRangeAliases)(Number(s.number.number), Number(s.number.last.number), `${s.street.name}${streetType}${streetSuffix}`, `${s.locality.name} ${s.state.abbreviation} ${s.postcode}`);
|
|
697
|
+
if (rval.sla_range_expanded.length === 0) {
|
|
698
|
+
delete rval.sla_range_expanded;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
682
701
|
if (count) {
|
|
683
702
|
if (index % Math.ceil(count / 100) === 0) {
|
|
684
|
-
logger('addr', JSON.stringify(rval, undefined, 2));
|
|
685
703
|
logger(`${index / count * 100}%`);
|
|
686
704
|
}
|
|
687
705
|
} else {
|
|
688
706
|
if (index % 10_000 === 0) {
|
|
689
|
-
logger('addr', JSON.stringify(rval, undefined, 2));
|
|
690
707
|
logger(`${index} rows`);
|
|
691
708
|
}
|
|
692
709
|
}
|
|
@@ -853,7 +870,14 @@ async function searchForAddress(searchString, p, pageSize = PAGE_SIZE) {
|
|
|
853
870
|
}
|
|
854
871
|
}, {
|
|
855
872
|
multi_match: {
|
|
856
|
-
|
|
873
|
+
// ADR 026: sla_range_expanded added HERE ONLY (not in the
|
|
874
|
+
// bool_prefix clause above). phrase_prefix uses best_fields
|
|
875
|
+
// max with tie_breaker default 0.0, so an absent field on
|
|
876
|
+
// non-range docs contributes 0 to the max — no P007-shape
|
|
877
|
+
// asymmetry. Adding sla_range_expanded to the bool_prefix
|
|
878
|
+
// fields would reintroduce the summation asymmetry ADR 025
|
|
879
|
+
// resolved. DO NOT move this field into the clause above.
|
|
880
|
+
fields: ['sla', 'ssla', 'sla_range_expanded'],
|
|
857
881
|
query: searchString,
|
|
858
882
|
// fuzziness: 'AUTO',
|
|
859
883
|
type: 'phrase_prefix',
|
|
@@ -1590,20 +1614,27 @@ async function getAddress(addressId) {
|
|
|
1590
1614
|
};
|
|
1591
1615
|
} catch (error_) {
|
|
1592
1616
|
error('error getting record from elastic search', error_);
|
|
1593
|
-
if (error_.body.found === false) {
|
|
1617
|
+
if (error_.body && error_.body.found === false) {
|
|
1594
1618
|
return {
|
|
1595
1619
|
statusCode: 404,
|
|
1596
1620
|
json: {
|
|
1597
1621
|
error: 'not found'
|
|
1598
1622
|
}
|
|
1599
1623
|
};
|
|
1600
|
-
} else if (error_.body.error.type === 'index_not_found_exception') {
|
|
1624
|
+
} else if (error_.body && error_.body.error && error_.body.error.type === 'index_not_found_exception') {
|
|
1601
1625
|
return {
|
|
1602
1626
|
statusCode: 503,
|
|
1603
1627
|
json: {
|
|
1604
1628
|
error: 'service unavailable'
|
|
1605
1629
|
}
|
|
1606
1630
|
};
|
|
1631
|
+
} else if (error_.displayName === 'RequestTimeout') {
|
|
1632
|
+
return {
|
|
1633
|
+
statusCode: 504,
|
|
1634
|
+
json: {
|
|
1635
|
+
error: 'gateway timeout'
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1607
1638
|
} else {
|
|
1608
1639
|
return {
|
|
1609
1640
|
statusCode: 500,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _nodeTest = require("node:test");
|
|
4
|
+
var _strict = _interopRequireDefault(require("node:assert/strict"));
|
|
5
|
+
var _addressService = require("./address-service.js");
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
// Regression test for P007 / issue #375 — see ADR 025.
|
|
8
|
+
// Sub-unit documents populate both `sla` and `ssla`; street-level documents
|
|
9
|
+
// historically populated only `sla`, which caused BM25 per-field score
|
|
10
|
+
// summation to favour sub-unit docs over exact street-level matches.
|
|
11
|
+
// The fix: always populate `ssla` (= `sla` when no flat) so scoring is
|
|
12
|
+
// symmetric across all documents.
|
|
13
|
+
|
|
14
|
+
const baseAuthorityContext = {
|
|
15
|
+
Authority_Code_LOCALITY_CLASS_AUT_psv: [{
|
|
16
|
+
CODE: 'G',
|
|
17
|
+
NAME: 'GAZETTED LOCALITY'
|
|
18
|
+
}],
|
|
19
|
+
Authority_Code_STREET_CLASS_AUT_psv: [{
|
|
20
|
+
CODE: 'C',
|
|
21
|
+
NAME: 'CONFIRMED'
|
|
22
|
+
}],
|
|
23
|
+
Authority_Code_STREET_TYPE_AUT_psv: [{
|
|
24
|
+
CODE: 'ROAD',
|
|
25
|
+
NAME: 'RD',
|
|
26
|
+
DESCRIPTION: 'RD'
|
|
27
|
+
}],
|
|
28
|
+
Authority_Code_STREET_SUFFIX_AUT_psv: [],
|
|
29
|
+
Authority_Code_FLAT_TYPE_AUT_psv: [{
|
|
30
|
+
CODE: 'UNIT',
|
|
31
|
+
NAME: 'UNIT'
|
|
32
|
+
}],
|
|
33
|
+
Authority_Code_LEVEL_TYPE_AUT_psv: [],
|
|
34
|
+
Authority_Code_GEOCODED_LEVEL_TYPE_AUT_psv: [],
|
|
35
|
+
Authority_Code_GEOCODE_TYPE_AUT_psv: [],
|
|
36
|
+
Authority_Code_GEOCODE_RELIABILITY_AUT_psv: [],
|
|
37
|
+
state: 'QLD',
|
|
38
|
+
stateName: 'QUEENSLAND'
|
|
39
|
+
};
|
|
40
|
+
function buildContext(streetLocalityPid, streetLocality, localityPid, locality) {
|
|
41
|
+
const context = {
|
|
42
|
+
...baseAuthorityContext,
|
|
43
|
+
streetLocalityIndexed: [],
|
|
44
|
+
localityIndexed: []
|
|
45
|
+
};
|
|
46
|
+
context.streetLocalityIndexed[streetLocalityPid] = streetLocality;
|
|
47
|
+
context.localityIndexed[localityPid] = locality;
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
const streetLocality = {
|
|
51
|
+
STREET_LOCALITY_PID: 'QLD180101',
|
|
52
|
+
DATE_CREATED: '2017-08-10',
|
|
53
|
+
DATE_RETIRED: '',
|
|
54
|
+
STREET_CLASS_CODE: 'C',
|
|
55
|
+
STREET_NAME: 'AERODROME',
|
|
56
|
+
STREET_TYPE_CODE: 'ROAD',
|
|
57
|
+
STREET_SUFFIX_CODE: '',
|
|
58
|
+
LOCALITY_PID: 'QLD69',
|
|
59
|
+
GNAF_STREET_PID: '3169537',
|
|
60
|
+
GNAF_STREET_CONFIDENCE: '2',
|
|
61
|
+
GNAF_RELIABILITY_CODE: '4'
|
|
62
|
+
};
|
|
63
|
+
const locality = {
|
|
64
|
+
LOCALITY_PID: 'QLD69',
|
|
65
|
+
DATE_CREATED: '2016-08-10',
|
|
66
|
+
DATE_RETIRED: '',
|
|
67
|
+
LOCALITY_NAME: 'APPLETHORPE',
|
|
68
|
+
PRIMARY_POSTCODE: '',
|
|
69
|
+
LOCALITY_CLASS_CODE: 'G',
|
|
70
|
+
STATE_PID: '3',
|
|
71
|
+
GNAF_LOCALITY_PID: '198011',
|
|
72
|
+
GNAF_RELIABILITY_CODE: '5'
|
|
73
|
+
};
|
|
74
|
+
function addressDetail(overrides = {}) {
|
|
75
|
+
return {
|
|
76
|
+
ADDRESS_DETAIL_PID: 'GAQLD163157353',
|
|
77
|
+
DATE_CREATED: '2010-04-21',
|
|
78
|
+
DATE_LAST_MODIFIED: '2018-08-03',
|
|
79
|
+
DATE_RETIRED: '',
|
|
80
|
+
BUILDING_NAME: '',
|
|
81
|
+
LOT_NUMBER_PREFIX: '',
|
|
82
|
+
LOT_NUMBER: '',
|
|
83
|
+
LOT_NUMBER_SUFFIX: '',
|
|
84
|
+
FLAT_TYPE_CODE: '',
|
|
85
|
+
FLAT_NUMBER_PREFIX: '',
|
|
86
|
+
FLAT_NUMBER: '',
|
|
87
|
+
FLAT_NUMBER_SUFFIX: '',
|
|
88
|
+
LEVEL_TYPE_CODE: '',
|
|
89
|
+
LEVEL_NUMBER_PREFIX: '',
|
|
90
|
+
LEVEL_NUMBER: '',
|
|
91
|
+
LEVEL_NUMBER_SUFFIX: '',
|
|
92
|
+
NUMBER_FIRST_PREFIX: '',
|
|
93
|
+
NUMBER_FIRST: '42',
|
|
94
|
+
NUMBER_FIRST_SUFFIX: '',
|
|
95
|
+
NUMBER_LAST_PREFIX: '',
|
|
96
|
+
NUMBER_LAST: '',
|
|
97
|
+
NUMBER_LAST_SUFFIX: '',
|
|
98
|
+
STREET_LOCALITY_PID: 'QLD180101',
|
|
99
|
+
LOCATION_DESCRIPTION: '',
|
|
100
|
+
LOCALITY_PID: 'QLD69',
|
|
101
|
+
ALIAS_PRINCIPAL: 'P',
|
|
102
|
+
POSTCODE: '4378',
|
|
103
|
+
PRIVATE_STREET: '',
|
|
104
|
+
LEGAL_PARCEL_ID: '',
|
|
105
|
+
CONFIDENCE: '0',
|
|
106
|
+
ADDRESS_SITE_PID: '',
|
|
107
|
+
LEVEL_GEOCODED_CODE: '',
|
|
108
|
+
PROPERTY_PID: '',
|
|
109
|
+
GNAF_PROPERTY_PID: '',
|
|
110
|
+
PRIMARY_SECONDARY: '',
|
|
111
|
+
...overrides
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
(0, _nodeTest.describe)('P007 — ssla population (ADR 025)', () => {
|
|
115
|
+
(0, _nodeTest.it)('populates ssla equal to sla for a street-level address with no sub-unit', () => {
|
|
116
|
+
const context = buildContext('QLD180101', streetLocality, 'QLD69', locality);
|
|
117
|
+
const mapped = (0, _addressService.mapAddressDetails)(addressDetail(), context, 1, 1);
|
|
118
|
+
_strict.default.equal(typeof mapped.sla, 'string', 'sla must be present on every indexed address');
|
|
119
|
+
_strict.default.equal(mapped.ssla, mapped.sla, 'ssla must equal sla when the address has no sub-unit, so BM25 per-field ' + 'score summation across [sla, ssla] is symmetric across documents');
|
|
120
|
+
});
|
|
121
|
+
(0, _nodeTest.it)('populates ssla from the short-form address for a sub-unit', () => {
|
|
122
|
+
const context = buildContext('QLD180101', streetLocality, 'QLD69', locality);
|
|
123
|
+
const mapped = (0, _addressService.mapAddressDetails)(addressDetail({
|
|
124
|
+
FLAT_TYPE_CODE: 'UNIT',
|
|
125
|
+
FLAT_NUMBER: '1'
|
|
126
|
+
}), context, 1, 1);
|
|
127
|
+
_strict.default.ok(mapped.ssla && mapped.ssla !== mapped.sla, 'for a sub-unit, ssla should be the short-form address (distinct from sla)');
|
|
128
|
+
_strict.default.match(mapped.sla, /UNIT 1/, 'sub-unit sla includes the unit prefix');
|
|
129
|
+
_strict.default.doesNotMatch(mapped.ssla, /UNIT 1/, 'sub-unit ssla strips the unit prefix (preserving slash-form matching)');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.SPAN_CAP = void 0;
|
|
7
|
+
exports.expandRangeAliases = expandRangeAliases;
|
|
8
|
+
// ADR 026: range-number address expansion via multi-valued text alias field.
|
|
9
|
+
// Pure helper — no I/O, no OpenSearch dependency. Invoked by `mapToMla` in
|
|
10
|
+
// `service/address-service.js` when a G-NAF address has NUMBER_LAST set.
|
|
11
|
+
// See docs/problems/015-range-number-addresses-not-searchable-by-base-number.open.md
|
|
12
|
+
// for the range-distribution measurements that justify SPAN_CAP = 20.
|
|
13
|
+
|
|
14
|
+
const SPAN_CAP = exports.SPAN_CAP = 20;
|
|
15
|
+
function expandRangeAliases(first, last, streetPart, localityPart) {
|
|
16
|
+
if (!Number.isInteger(first) || !Number.isInteger(last)) return [];
|
|
17
|
+
if (first <= 0 || last <= 0) return [];
|
|
18
|
+
if (first >= last) return [];
|
|
19
|
+
if (last - first > SPAN_CAP) return [];
|
|
20
|
+
const aliases = [];
|
|
21
|
+
for (let n = first; n <= last; n++) {
|
|
22
|
+
aliases.push(`${n} ${streetPart}, ${localityPart}`);
|
|
23
|
+
}
|
|
24
|
+
return aliases;
|
|
25
|
+
}
|
|
@@ -999,6 +999,12 @@ function startRest2Server() {
|
|
|
999
999
|
}],
|
|
1000
1000
|
headers: {
|
|
1001
1001
|
etag: `"${_version.version}"`,
|
|
1002
|
+
// Long-lived by design (P018 parked). New rels are added
|
|
1003
|
+
// infrequently and every client page load fetches this for
|
|
1004
|
+
// HATEOAS discovery; a short TTL would cost an origin
|
|
1005
|
+
// round-trip per request. When the rel set does change,
|
|
1006
|
+
// request a RapidAPI CF purge (natural expiry up to 7 days
|
|
1007
|
+
// per P017 close notes).
|
|
1002
1008
|
'cache-control': `public, max-age=${ONE_WEEK}`
|
|
1003
1009
|
}
|
|
1004
1010
|
};
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mountainpass/addressr",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Australian Address Validation, Search and Autocomplete",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mountain Pass",
|
|
@@ -107,6 +107,8 @@
|
|
|
107
107
|
"dotest:cli2:geo": "ADDRESSR_ENABLE_GEO=1 ES_INDEX_NAME=test COVERED_STATES=OT TEST_PROFILE=cli2 cucumber-js -p cli2 -- --harmony_async_iteration",
|
|
108
108
|
"cover:cli:geo": "nyc --report-dir coverage/cli --temp-dir coverage/cli/.nyc_output npm run test:cli:nogeo",
|
|
109
109
|
"test:mcp:smoke": "node --test test/mcp/smoke.test.mjs",
|
|
110
|
+
"test:precommit": "node --test test/precommit/*.test.mjs",
|
|
111
|
+
"test:js": "node --test test/js/__tests__/*.test.mjs",
|
|
110
112
|
"test:nodejs:QLD:nogeo": "PORT=$npm_package_config_localport ES_INDEX_NAME=test COVERED_STATES=QLD DEBUG=error,api,express:*,swagger-tools*,test,es TEST_PROFILE=default cucumber-js -p default -- --harmony_async_iteration",
|
|
111
113
|
"test:nodejs:QLD:geo": "PORT=$npm_package_config_localport ADDRESSR_ENABLE_GEO=1 ES_INDEX_NAME=test-geo COVERED_STATES=QLD DEBUG=error,api,express:*,swagger-tools*,test,es TEST_PROFILE=default NODE_OPTIONS=--max_old_space_size=8196 cucumber-js -p default -- --harmony_async_iteration",
|
|
112
114
|
"prebuildX": "npm run genversion && cat ./templates/LICENSE.md | envsubst '${PRODUCT},${VERSION},${COMPANY},${YEAR}' > ./LICENSE.md",
|
|
@@ -122,7 +124,8 @@
|
|
|
122
124
|
"docker:push": "docker push \"mountainpass/addressr:${npm_package_version}\"",
|
|
123
125
|
"postdocker:push": "docker push \"mountainpass/addressr:latest\"",
|
|
124
126
|
"check-licenses": "license-checker --production --onlyAllow 'MIT;Apache-2.0;ISC;Custom: http://github.com/substack/node-bufferlist;Unlicense;BSD-2-Clause;BSD-3-Clause;WTFPL;0BSD;MIT*;Python-2.0;MPL-2.0;BlueOak-1.0.0' --summary",
|
|
125
|
-
"pre-commit": "lint-staged && npm run check-licenses",
|
|
127
|
+
"pre-commit": "lint-staged && npm run check-licenses && npm run check:not-cli2-tags && npm run test:js",
|
|
128
|
+
"check:not-cli2-tags": "node scripts/check-not-cli2-tags.mjs",
|
|
126
129
|
"check-deps": "dry-aged-deps --check",
|
|
127
130
|
"test:performance": "k6 run --out csv=target/stress.csv test/k6/script.js",
|
|
128
131
|
"add-changeset": "changeset add --open",
|