@mountainpass/addressr 2.1.0 → 2.1.2

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.
@@ -113,6 +113,9 @@ async function initIndex(esClient, clear, synonyms) {
113
113
  },
114
114
  confidence: {
115
115
  type: 'integer'
116
+ },
117
+ locality_pid: {
118
+ type: 'keyword'
116
119
  }
117
120
  }
118
121
  }
@@ -9,6 +9,8 @@ exports.fetchGnafFile = fetchGnafFile;
9
9
  exports.getAddress = getAddress;
10
10
  exports.getAddresses = getAddresses;
11
11
  exports.getLocality = getLocality;
12
+ exports.getPostcode = getPostcode;
13
+ exports.getState = getState;
12
14
  exports.loadGnaf = loadGnaf;
13
15
  exports.mapAddressDetails = mapAddressDetails;
14
16
  exports.searchForAddress = searchForAddress;
@@ -735,7 +737,8 @@ async function loadAddressDetails(file, expectedCount, context, {
735
737
  sla,
736
738
  ssla,
737
739
  structured,
738
- confidence: structured.structured.confidence
740
+ confidence: structured.structured.confidence,
741
+ locality_pid: row.LOCALITY_PID
739
742
  });
740
743
  }
741
744
  if (indexingBody.length > 0) {
@@ -934,20 +937,29 @@ async function getLocality(pid) {
934
937
  return resp;
935
938
  }
936
939
  async function searchForPostcode(searchString) {
940
+ const query = searchString && searchString.length > 0 ? {
941
+ bool: {
942
+ filter: [{
943
+ prefix: {
944
+ postcodes: searchString
945
+ }
946
+ }]
947
+ }
948
+ } : {
949
+ bool: {
950
+ filter: [{
951
+ exists: {
952
+ field: 'postcodes'
953
+ }
954
+ }]
955
+ }
956
+ };
937
957
  const searchResp = await globalThis.esClient.search({
938
958
  index: _elasticsearch.ES_LOCALITY_INDEX_NAME,
939
959
  body: {
940
960
  from: 0,
941
961
  size: 0,
942
- query: {
943
- bool: {
944
- filter: [{
945
- prefix: {
946
- postcodes: searchString
947
- }
948
- }]
949
- }
950
- },
962
+ query,
951
963
  aggs: {
952
964
  postcodes: {
953
965
  terms: {
@@ -969,6 +981,43 @@ async function searchForPostcode(searchString) {
969
981
  logger('postcode hits', JSON.stringify(searchResp.body.aggregations, undefined, 2));
970
982
  return searchResp;
971
983
  }
984
+ async function getPostcode(postcode) {
985
+ const searchResp = await globalThis.esClient.search({
986
+ index: _elasticsearch.ES_LOCALITY_INDEX_NAME,
987
+ body: {
988
+ size: 100,
989
+ query: {
990
+ term: {
991
+ postcodes: postcode
992
+ }
993
+ },
994
+ _source: ['locality_name', 'locality_pid']
995
+ }
996
+ });
997
+ return searchResp;
998
+ }
999
+ async function getState(abbreviation) {
1000
+ const searchResp = await globalThis.esClient.search({
1001
+ index: _elasticsearch.ES_LOCALITY_INDEX_NAME,
1002
+ body: {
1003
+ size: 0,
1004
+ query: {
1005
+ term: {
1006
+ state_abbreviation: abbreviation.toUpperCase()
1007
+ }
1008
+ },
1009
+ aggs: {
1010
+ state_name: {
1011
+ terms: {
1012
+ field: 'state_name',
1013
+ size: 1
1014
+ }
1015
+ }
1016
+ }
1017
+ }
1018
+ });
1019
+ return searchResp;
1020
+ }
972
1021
  async function searchForState(searchString) {
973
1022
  const query = searchString ? {
974
1023
  bool: {
@@ -1532,10 +1581,12 @@ async function getAddress(addressId) {
1532
1581
  });
1533
1582
  // TODO: store hash in address
1534
1583
  const hash = _nodeCrypto.default.createHash('md5').update(JSON.stringify(json)).digest('hex');
1584
+ const localityPid = jsonX.body._source.locality_pid;
1535
1585
  return {
1536
1586
  link,
1537
1587
  json,
1538
- hash
1588
+ hash,
1589
+ localityPid
1539
1590
  };
1540
1591
  } catch (error_) {
1541
1592
  error('error getting record from elastic search', error_);
@@ -47,10 +47,37 @@ function startRest2Server() {
47
47
  const {
48
48
  json,
49
49
  hash,
50
- statusCode
50
+ statusCode,
51
+ localityPid
51
52
  } = await (0, _addressService.getAddress)(pid);
53
+ const links = [];
54
+ if (localityPid) {
55
+ links.push({
56
+ rel: 'related',
57
+ uri: `/localities/${localityPid}`,
58
+ title: json.structured?.locality?.name || 'Locality'
59
+ });
60
+ }
61
+ if (json.structured) {
62
+ const s = json.structured;
63
+ if (s.postcode) {
64
+ links.push({
65
+ rel: 'related',
66
+ uri: `/postcodes/${s.postcode}`,
67
+ title: `Postcode ${s.postcode}`
68
+ });
69
+ }
70
+ if (s.state && s.state.abbreviation) {
71
+ links.push({
72
+ rel: 'related',
73
+ uri: `/states/${s.state.abbreviation}`,
74
+ title: s.state.name
75
+ });
76
+ }
77
+ }
52
78
  return {
53
79
  body: json,
80
+ links,
54
81
  headers: {
55
82
  etag: `"${_version.version}-${hash}"`,
56
83
  'cache-control': `public, max-age=${ONE_WEEK}`
@@ -114,9 +141,25 @@ function startRest2Server() {
114
141
  }) => {
115
142
  const resp = await (0, _addressService.getLocality)(pid);
116
143
  const source = resp.body._source;
144
+ const links = [];
145
+ if (source.primary_postcode) {
146
+ links.push({
147
+ rel: 'related',
148
+ uri: `/postcodes/${source.primary_postcode}`,
149
+ title: `Postcode ${source.primary_postcode}`
150
+ });
151
+ }
152
+ if (source.state_abbreviation) {
153
+ links.push({
154
+ rel: 'related',
155
+ uri: `/states/${source.state_abbreviation}`,
156
+ title: source.state_name
157
+ });
158
+ }
117
159
  const hash = _nodeCrypto.default.createHash('md5').update(JSON.stringify(source)).digest('hex');
118
160
  return {
119
161
  body: source,
162
+ links,
120
163
  headers: {
121
164
  etag: `"${_version.version}-${hash}"`,
122
165
  'cache-control': `public, max-age=${ONE_WEEK}`
@@ -177,38 +220,56 @@ function startRest2Server() {
177
220
  }]
178
221
  });
179
222
  const postcodesType = waycharter.registerCollection({
223
+ itemPath: '/:postcode',
224
+ itemLoader: async ({
225
+ postcode
226
+ }) => {
227
+ const result = await (0, _addressService.getPostcode)(postcode);
228
+ const hits = result.body.hits.hits;
229
+ const localities = hits.map(h => ({
230
+ name: h._source.locality_name
231
+ }));
232
+ const links = hits.map(h => ({
233
+ rel: 'related',
234
+ uri: `/localities/${h._source.locality_pid}`,
235
+ title: h._source.locality_name
236
+ }));
237
+ const body = {
238
+ postcode,
239
+ localities
240
+ };
241
+ const hash = _nodeCrypto.default.createHash('md5').update(JSON.stringify(body)).digest('hex');
242
+ return {
243
+ body,
244
+ links,
245
+ headers: {
246
+ etag: `"${_version.version}-${hash}"`,
247
+ 'cache-control': `public, max-age=${ONE_WEEK}`
248
+ },
249
+ status: 200
250
+ };
251
+ },
180
252
  collectionPath: '/postcodes',
181
253
  collectionLoader: async ({
182
254
  q
183
255
  }) => {
184
- if (q && q.length > 2) {
185
- const result = await (0, _addressService.searchForPostcode)(q);
186
- const buckets = result.body.aggregations.postcodes.buckets;
187
- const body = buckets.map(bucket => ({
188
- postcode: bucket.key,
189
- localities: bucket.localities.buckets.map(l => ({
190
- name: l.key
191
- }))
192
- }));
193
- const responseHash = _nodeCrypto.default.createHash('md5').update(JSON.stringify(body)).digest('hex');
194
- return {
195
- body,
196
- hasMore: false,
197
- headers: {
198
- etag: `"${_version.version}-${responseHash}"`,
199
- 'cache-control': `public, max-age=${ONE_WEEK}`
200
- }
201
- };
202
- } else {
203
- return {
204
- body: [],
205
- hasMore: false,
206
- headers: {
207
- etag: `"${_version.version}"`,
208
- 'cache-control': `public, max-age=${ONE_WEEK}`
209
- }
210
- };
211
- }
256
+ const result = await (0, _addressService.searchForPostcode)(q || '');
257
+ const buckets = result.body.aggregations.postcodes.buckets;
258
+ const body = buckets.map(bucket => ({
259
+ postcode: bucket.key,
260
+ localities: bucket.localities.buckets.map(l => ({
261
+ name: l.key
262
+ }))
263
+ }));
264
+ const responseHash = _nodeCrypto.default.createHash('md5').update(JSON.stringify(body)).digest('hex');
265
+ return {
266
+ body,
267
+ hasMore: false,
268
+ headers: {
269
+ etag: `"${_version.version}-${responseHash}"`,
270
+ 'cache-control': `public, max-age=${ONE_WEEK}`
271
+ }
272
+ };
212
273
  },
213
274
  filters: [{
214
275
  rel: 'https://addressr.io/rels/postcode-search',
@@ -216,11 +277,31 @@ function startRest2Server() {
216
277
  }]
217
278
  });
218
279
  const statesType = waycharter.registerCollection({
280
+ itemPath: '/:abbreviation',
281
+ itemLoader: async ({
282
+ abbreviation
283
+ }) => {
284
+ const result = await (0, _addressService.getState)(abbreviation);
285
+ const stateName = result.body.aggregations.state_name.buckets[0]?.key || abbreviation.toUpperCase();
286
+ const body = {
287
+ abbreviation: abbreviation.toUpperCase(),
288
+ name: stateName
289
+ };
290
+ const hash = _nodeCrypto.default.createHash('md5').update(JSON.stringify(body)).digest('hex');
291
+ return {
292
+ body,
293
+ headers: {
294
+ etag: `"${_version.version}-${hash}"`,
295
+ 'cache-control': `public, max-age=${ONE_WEEK}`
296
+ },
297
+ status: 200
298
+ };
299
+ },
219
300
  collectionPath: '/states',
220
301
  collectionLoader: async ({
221
302
  q
222
303
  }) => {
223
- const result = await (0, _addressService.searchForState)(q && q.length > 1 ? q : undefined);
304
+ const result = await (0, _addressService.searchForState)(q || undefined);
224
305
  const buckets = result.body.aggregations.states.buckets;
225
306
  const body = buckets.map(bucket => ({
226
307
  abbreviation: bucket.key,
@@ -256,12 +337,199 @@ function startRest2Server() {
256
337
  };
257
338
  }
258
339
  });
340
+ waycharter.registerResourceType({
341
+ path: '/api-docs',
342
+ loader: async () => {
343
+ const spec = {
344
+ openapi: '3.0.3',
345
+ info: {
346
+ title: 'Addressr by Mountain Pass',
347
+ description: 'Free Australian Address Validation, Search and Autocomplete. This OpenAPI spec is supplementary — the HATEOAS link-driven API is the authoritative contract.',
348
+ version: _version.version
349
+ },
350
+ paths: {
351
+ '/addresses': {
352
+ get: {
353
+ summary: 'Search Addresses',
354
+ operationId: 'searchAddresses',
355
+ parameters: [{
356
+ name: 'q',
357
+ in: 'query',
358
+ required: true,
359
+ schema: {
360
+ type: 'string',
361
+ minLength: 3
362
+ },
363
+ description: 'Address search query'
364
+ }],
365
+ responses: {
366
+ 200: {
367
+ description: 'List of matching addresses'
368
+ }
369
+ }
370
+ }
371
+ },
372
+ '/addresses/{pid}': {
373
+ get: {
374
+ summary: 'Get Address',
375
+ operationId: 'getAddress',
376
+ parameters: [{
377
+ name: 'pid',
378
+ in: 'path',
379
+ required: true,
380
+ schema: {
381
+ type: 'string'
382
+ },
383
+ description: 'Address persistent identifier'
384
+ }],
385
+ responses: {
386
+ 200: {
387
+ description: 'Address details with structured data'
388
+ }
389
+ }
390
+ }
391
+ },
392
+ '/localities': {
393
+ get: {
394
+ summary: 'Search Localities',
395
+ operationId: 'searchLocalities',
396
+ parameters: [{
397
+ name: 'q',
398
+ in: 'query',
399
+ required: true,
400
+ schema: {
401
+ type: 'string',
402
+ minLength: 2
403
+ },
404
+ description: 'Locality/suburb name search query'
405
+ }],
406
+ responses: {
407
+ 200: {
408
+ description: 'List of matching localities'
409
+ }
410
+ }
411
+ }
412
+ },
413
+ '/localities/{pid}': {
414
+ get: {
415
+ summary: 'Get Locality',
416
+ operationId: 'getLocality',
417
+ parameters: [{
418
+ name: 'pid',
419
+ in: 'path',
420
+ required: true,
421
+ schema: {
422
+ type: 'string'
423
+ },
424
+ description: 'Locality persistent identifier'
425
+ }],
426
+ responses: {
427
+ 200: {
428
+ description: 'Locality details'
429
+ }
430
+ }
431
+ }
432
+ },
433
+ '/postcodes': {
434
+ get: {
435
+ summary: 'Search Postcodes',
436
+ operationId: 'searchPostcodes',
437
+ parameters: [{
438
+ name: 'q',
439
+ in: 'query',
440
+ required: false,
441
+ schema: {
442
+ type: 'string'
443
+ },
444
+ description: 'Postcode prefix search query. Omit to list all postcodes.'
445
+ }],
446
+ responses: {
447
+ 200: {
448
+ description: 'List of matching postcodes with associated localities'
449
+ }
450
+ }
451
+ }
452
+ },
453
+ '/postcodes/{postcode}': {
454
+ get: {
455
+ summary: 'Get Postcode',
456
+ operationId: 'getPostcode',
457
+ parameters: [{
458
+ name: 'postcode',
459
+ in: 'path',
460
+ required: true,
461
+ schema: {
462
+ type: 'string'
463
+ },
464
+ description: 'Australian postcode'
465
+ }],
466
+ responses: {
467
+ 200: {
468
+ description: 'Postcode details with associated localities'
469
+ }
470
+ }
471
+ }
472
+ },
473
+ '/states': {
474
+ get: {
475
+ summary: 'Search States',
476
+ operationId: 'searchStates',
477
+ parameters: [{
478
+ name: 'q',
479
+ in: 'query',
480
+ required: false,
481
+ schema: {
482
+ type: 'string'
483
+ },
484
+ description: 'State name or abbreviation search. Omit to list all states.'
485
+ }],
486
+ responses: {
487
+ 200: {
488
+ description: 'List of matching states and territories'
489
+ }
490
+ }
491
+ }
492
+ },
493
+ '/states/{abbreviation}': {
494
+ get: {
495
+ summary: 'Get State',
496
+ operationId: 'getState',
497
+ parameters: [{
498
+ name: 'abbreviation',
499
+ in: 'path',
500
+ required: true,
501
+ schema: {
502
+ type: 'string'
503
+ },
504
+ description: 'State/territory abbreviation (e.g., NSW, VIC)'
505
+ }],
506
+ responses: {
507
+ 200: {
508
+ description: 'State/territory details'
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ };
515
+ return {
516
+ body: spec,
517
+ headers: {
518
+ 'cache-control': `public, max-age=${ONE_WEEK}`,
519
+ 'content-type': 'application/json'
520
+ }
521
+ };
522
+ }
523
+ });
259
524
  waycharter.registerResourceType({
260
525
  path: '/',
261
526
  loader: async () => {
262
527
  return {
263
528
  body: {},
264
529
  links: [...addressesType.additionalPaths, ...localitiesType.additionalPaths, ...postcodesType.additionalPaths, ...statesType.additionalPaths, {
530
+ rel: 'https://addressr.io/rels/api-docs',
531
+ uri: '/api-docs'
532
+ }, {
265
533
  rel: 'https://addressr.io/rels/health',
266
534
  uri: '/health'
267
535
  }],
package/lib/version.js CHANGED
@@ -5,4 +5,4 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.version = void 0;
7
7
  // Generated by genversion.
8
- const version = exports.version = '2.1.0';
8
+ const version = exports.version = '2.1.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mountainpass/addressr",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Australian Address Validation, Search and Autocomplete",
5
5
  "author": {
6
6
  "name": "Mountain Pass",