From 576d1a1809e7388739bbb18a4f90b15d9a02ce47 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Thu, 15 Dec 2016 15:24:16 +0100 Subject: [PATCH] interpolation: integration v1 --- middleware/interpolate.js | 136 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + routes/v1.js | 3 + service/interpolation.js | 58 ++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 middleware/interpolate.js create mode 100644 service/interpolation.js diff --git a/middleware/interpolate.js b/middleware/interpolate.js new file mode 100644 index 00000000..d9974a1b --- /dev/null +++ b/middleware/interpolate.js @@ -0,0 +1,136 @@ + +var async = require('async'); +var service = require('../service/interpolation')(); +var logger = require( 'pelias-logger' ).get( 'api' ); + +/** +example response from interpolation web service: +{ + type: 'Feature', + properties: { + type: 'interpolated', + source: 'mixed', + number: '17', + lat: -41.2887032, + lon: 174.767089 + }, + geometry: { + type: 'Point', + coordinates: [ 174.767089, -41.2887032 ] + } +} +**/ + +function setup() { + return function middleware(req, res, next) { + + // no-op, user did not request an address + if( !isAddressQuery( req ) ){ + return next(); + } + + // bind parsed_text variables to function call + var bound = interpolate.bind( null, req.clean.parsed_text ); + + // perform interpolations asynchronously for all relevant hits + var timer = (new Date()).getTime(); + async.map( res.data, bound, function( err, results ){ + + // update res.data with the mapped values + if( !err ){ + res.data = results; + } + + // log the execution time, continue + logger.info( '[interpolation] [took]', (new Date()).getTime() - timer, 'ms' ); + next(); + }); + }; +} + +function interpolate( parsed_text, hit, cb ){ + + // no-op, this hit is not from the 'street' layer + // note: no network request is performed. + if( !hit || hit.layer !== 'street' ){ + return cb( null, hit ); + } + + // query variables + var coord = hit.center_point; + var number = parsed_text.number; + var street = hit.address_parts.street || parsed_text.street; + + // query interpolation service + service.query( coord, number, street, function( err, data ){ + + // an error occurred + // note: leave this hit unmodified + if( err ){ + logger.error( '[interpolation] [error]', err ); + return cb( null, hit ); + } + + // invalid / not useful response + // note: leave this hit unmodified + if( !data || !data.hasOwnProperty('properties') ){ + logger.info( '[interpolation] [miss]', parsed_text ); + return cb( null, hit ); + } + + // the interpolation service returned a valid result + // note: we now merge thos values with the existing 'street' record. + logger.info( '[interpolation] [hit]', parsed_text, data ); + + // safety first! + try { + + // -- metatdata -- + hit.layer = 'address'; + hit.match_type = 'interpolated'; + + // -- name -- + hit.name.default = data.properties.number + ' ' + hit.name.default; + + // -- source -- + var source = 'mixed'; + if( data.properties.source === 'osm' ){ source = 'openstreetmap'; } + else if( data.properties.source === 'oa' ){ source = 'openaddresses'; } + hit.source = source; + + // -- source_id -- + hit.source_id = 'derived:'+ hit.source_id; + + // -- address_parts -- + hit.address_parts.number = data.properties.number; + + // -- geo -- + hit.center_point = { + lat: data.properties.lat, + lon: data.properties.lon + }; + + // -- bbox -- + delete hit.bounding_box; + + // return the modified hit + return cb( null, hit ); + + // a syntax error occurred in the code above (this shouldn't happen!) + // note: the hit object may be partially modified, could possibly be invalid + } catch( e ){ + logger.error( '[interpolation] [error]', e, e.stack ); + return cb( null, hit ); + } + }); +} + +// boolean function to check if an address was requested by the user +function isAddressQuery( req ){ + return req && req.hasOwnProperty('clean') && + req.clean.hasOwnProperty('parsed_text') && + req.clean.parsed_text.hasOwnProperty('number') && + req.clean.parsed_text.hasOwnProperty('street'); +} + +module.exports = setup; diff --git a/package.json b/package.json index 398ce8c8..95b9aa53 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "pelias-query": "8.12.0", "pelias-text-analyzer": "1.7.0", "stats-lite": "2.0.3", + "superagent": "^3.2.1", "through2": "^2.0.3" }, "devDependencies": { diff --git a/routes/v1.js b/routes/v1.js index 89b86613..a18dc797 100644 --- a/routes/v1.js +++ b/routes/v1.js @@ -44,6 +44,7 @@ var postProc = { confidenceScoresReverse: require('../middleware/confidenceScoreReverse'), accuracy: require('../middleware/accuracy'), dedupe: require('../middleware/dedupe'), + interpolate: require('../middleware/interpolate'), localNamingConventions: require('../middleware/localNamingConventions'), renamePlacenames: require('../middleware/renamePlacenames'), geocodeJSON: require('../middleware/geocodeJSON'), @@ -86,6 +87,7 @@ function addRoutes(app, peliasConfig) { postProc.confidenceScores(peliasConfig), postProc.confidenceScoresFallback(), postProc.dedupe(), + postProc.interpolate(), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), @@ -104,6 +106,7 @@ function addRoutes(app, peliasConfig) { postProc.confidenceScores(peliasConfig), postProc.confidenceScoresFallback(), postProc.dedupe(), + postProc.interpolate(), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), diff --git a/service/interpolation.js b/service/interpolation.js new file mode 100644 index 00000000..4e6fe405 --- /dev/null +++ b/service/interpolation.js @@ -0,0 +1,58 @@ + +var logger = require( 'pelias-logger' ).get( 'api' ); + +/** + + street address interpolation service + + see: https://github.com/pelias/interpolation + +**/ + +/** + RequireTransport + + allows the api to be used by simply requiring the module +**/ +function RequireTransport( addressDbPath, streetDbPath ){ + try { + var lib = require('pelias-interpolation'); + this.query = lib.api.search( addressDbPath, streetDbPath ); + } catch( e ){ + logger.error( 'RequireTransport: failed to connect to interpolation service' ); + } +} +RequireTransport.prototype.query = function( coord, number, street, cb ){ + throw new Error( 'transport not connected' ); +}; + +/** + HttpTransport + + allows the api to be used via a remote web service +**/ +function HttpTransport( host ){ + var request = require('superagent'); + this.query = function( coord, number, street, cb ){ + request + .get( host + '/search/geojson' ) + .set( 'Accept', 'application/json' ) + .query({ lat: coord.lat, lon: coord.lon, number: number, street: street }) + .end( function( err, res ){ + if( err || !res ){ return cb( err ); } + return cb( null, res.body ); + }); + }; +} +HttpTransport.prototype.query = function( coord, number, street, cb ){ + throw new Error( 'transport not connected' ); +}; + +/** + Setup + + allows instantiation of transport depending on configuration and preference +**/ +module.exports = function setup(){ + return new HttpTransport( 'http://interpolation.wiz.co.nz' ); +};