diff --git a/README.md b/README.md
index 0991c143..c3287227 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ The API recognizes the following properties under the top-level `api` key in you
|`accessLog`|*no*||name of the format to use for access logs; may be any one of the [predefined values](https://github.com/expressjs/morgan#predefined-formats) in the `morgan` package. Defaults to `"common"`; if set to `false`, or an otherwise falsy value, disables access-logging entirely.|
|`services`|*no*||service definitions for [point-in-polygon](https://github.com/pelias/pip-service), [libpostal](https://github.com/whosonfirst/go-whosonfirst-libpostal), [placeholder](https://github.com/pelias/placeholder), and [interpolation](https://github.com/pelias/interpolation) services. If missing (which is not recommended), the services will not be called.|
|`defaultParameters.focus.point.lon`
`defaultParameters.focus.point.lat`|no | |default coordinates for focus point
+|`targets.layers_by_source`
`targets.source_aliases`
`targets.layer_aliases`|no | |custom values for which `sources` and `layers` the API accepts ([more info](https://github.com/pelias/api/pull/1131)).
A good starting configuration file includes this section (fill in the service and Elasticsearch hosts as needed):
diff --git a/helper/TypeMapping.js b/helper/TypeMapping.js
new file mode 100644
index 00000000..97d87a26
--- /dev/null
+++ b/helper/TypeMapping.js
@@ -0,0 +1,146 @@
+const _ = require('lodash');
+const elasticsearch = require('elasticsearch');
+
+var TypeMapping = function(){
+
+ // A list of all sources
+ this.sources = [];
+
+ // A list of alternate names for sources, mostly used to save typing
+ this.source_aliases = {};
+
+ // A list of all layers
+ this.layers = [];
+
+ /*
+ * A list of all layers in each source. This is used for convenience elswhere
+ * and to determine when a combination of source and layer parameters is
+ * not going to match any records and will return no results.
+ */
+ this.layers_by_source = {};
+
+ /*
+ * A list of layer aliases that can be used to support specific use cases
+ * (like coarse geocoding) * or work around the fact that different sources
+ * may have layers that mean the same thing but have a different name
+ */
+ this.layer_aliases = {};
+
+ /*
+ * An object that contains all sources or aliases. The key is the source or alias,
+ * the value is either that source, or the canonical name for that alias if it's an alias.
+ */
+ this.source_mapping = {};
+
+ /*
+ * An object that has a key for each possible layer or alias,
+ * and returns either that layer, or gall the layers in the alias
+ */
+ this.layer_mapping = {};
+};
+
+TypeMapping.addStandardTargetsToAliases = function(standard, aliases) {
+ var combined = _.extend({}, aliases);
+ standard.forEach(function(target) {
+ if (combined[target] === undefined) {
+ combined[target] = [target];
+ }
+ });
+
+ return combined;
+};
+
+// source alias setter
+TypeMapping.prototype.setSourceAliases = function( aliases ){
+ this.source_aliases = aliases;
+};
+
+// layers-by-source alias setter
+TypeMapping.prototype.setLayersBySource = function( lbs ){
+ this.layers_by_source = lbs;
+};
+
+// layer alias setter
+TypeMapping.prototype.setLayerAliases = function( aliases ){
+ this.layer_aliases = aliases;
+};
+
+// generate mappings after setters have been run
+TypeMapping.prototype.generateMappings = function(){
+ this.sources = Object.keys( this.layers_by_source );
+ this.source_mapping = TypeMapping.addStandardTargetsToAliases(this.sources, this.source_aliases);
+ this.layers = _.uniq(Object.keys(this.layers_by_source).reduce(function(acc, key) {
+ return acc.concat(this.layers_by_source[key]);
+ }.bind(this), []));
+ this.layer_mapping = TypeMapping.addStandardTargetsToAliases(this.layers, this.layer_aliases);
+};
+
+// load values from targets block
+TypeMapping.prototype.loadTargets = function( targetsBlock ){
+
+ if( !_.isObject(targetsBlock) ){ return; }
+
+ // set values from targets block
+ this.setSourceAliases( targetsBlock.source_aliases || {} );
+ this.setLayersBySource( targetsBlock.layers_by_source || {} );
+ this.setLayerAliases( targetsBlock.layer_aliases || {} );
+
+ // generate the mappings
+ this.generateMappings();
+};
+
+// load values from either pelias config file or from elasticsearch
+TypeMapping.prototype.load = function( done ){
+
+ // load pelias config
+ const peliasConfigTargets = _.get(
+ require('pelias-config').generate(require('../schema')),
+ 'api.targets', {}
+ );
+
+ // load targets from config file
+ this.loadTargets( peliasConfigTargets );
+
+ // do not load values from elasticsearch
+ if( true !== peliasConfigTargets.auto_discover ){
+ if( 'function' === typeof done ){ done(); }
+ return;
+ }
+
+ if( 'function' === typeof done ){ done(); }
+ return;
+
+ // load values from elasticsearch
+
+ // create connection to elasticsearch
+ // const esclient = elasticsearch.Client(peliasConfig.esclient);
+
+ // const query = {
+ // requestCache: true,
+ // preference: '_replica_first',
+ // timeout: '10s',
+ // body: {
+ // aggs: {
+ // sources: {
+ // terms: {
+ // field: 'source',
+ // size: 100
+ // }
+ // },
+ // layers: {
+ // terms: {
+ // field: 'layer',
+ // size: 100
+ // }
+ // }
+ // },
+ // size: 0
+ // }
+ // };
+
+ // esclient.search( query, ( err, res ) => {
+ // console.error( err, res );
+ // });
+};
+
+module.exports = TypeMapping;
diff --git a/helper/type_mapping.js b/helper/type_mapping.js
index 41303583..987aa26a 100644
--- a/helper/type_mapping.js
+++ b/helper/type_mapping.js
@@ -1,86 +1,8 @@
-const _ = require('lodash');
+const TypeMapping = require('./TypeMapping');
-function addStandardTargetsToAliases(standard, aliases) {
- var combined = _.extend({}, aliases);
- standard.forEach(function(target) {
- if (combined[target] === undefined) {
- combined[target] = [target];
- }
- });
+// instantiate a new type mapping
+var tm = new TypeMapping();
+tm.load();
- return combined;
-}
-
-/*
- * Sources
- */
-
-// a list of all sources
-var SOURCES = ['openstreetmap', 'openaddresses', 'geonames', 'whosonfirst'];
-
-/*
- * A list of alternate names for sources, mostly used to save typing
- */
-var SOURCE_ALIASES = {
- 'osm': ['openstreetmap'],
- 'oa': ['openaddresses'],
- 'gn': ['geonames'],
- 'wof': ['whosonfirst']
-};
-
-/*
- * Create an object that contains all sources or aliases. The key is the source or alias,
- * the value is either that source, or the canonical name for that alias if it's an alias.
- */
-var SOURCE_MAPPING = addStandardTargetsToAliases(SOURCES, SOURCE_ALIASES);
-
-/*
- * Layers
- */
-
-/*
- * A list of all layers in each source. This is used for convenience elswhere
- * and to determine when a combination of source and layer parameters is
- * not going to match any records and will return no results.
- */
-var LAYERS_BY_SOURCE = {
- openstreetmap: [ 'address', 'venue', 'street' ],
- openaddresses: [ 'address' ],
- geonames: [ 'country','macroregion', 'region', 'county','localadmin',
- 'locality','borough', 'neighbourhood', 'venue' ],
- whosonfirst: [ 'continent', 'empire', 'country', 'dependency', 'macroregion', 'region',
- 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood', 'borough',
- 'neighbourhood', 'microhood', 'disputed', 'venue', 'postalcode',
- 'continent', 'ocean', 'marinearea']
-};
-
-/*
- * A list of layer aliases that can be used to support specific use cases
- * (like coarse geocoding) * or work around the fact that different sources
- * may have layers that mean the same thing but have a different name
- */
-var LAYER_ALIASES = {
- 'coarse': [ 'continent', 'empire', 'country', 'dependency', 'macroregion',
- 'region', 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood',
- 'borough', 'neighbourhood', 'microhood', 'disputed', 'postalcode',
- 'continent', 'ocean', 'marinearea']
-};
-
-// create a list of all layers by combining each entry from LAYERS_BY_SOURCE
-var LAYERS = _.uniq(Object.keys(LAYERS_BY_SOURCE).reduce(function(acc, key) {
- return acc.concat(LAYERS_BY_SOURCE[key]);
-}, []));
-
-/*
- * Create the an object that has a key for each possible layer or alias,
- * and returns either that layer, or all the layers in the alias
- */
-var LAYER_MAPPING = addStandardTargetsToAliases(LAYERS, LAYER_ALIASES);
-
-module.exports = {
- sources: SOURCES,
- layers: LAYERS,
- source_mapping: SOURCE_MAPPING,
- layer_mapping: LAYER_MAPPING,
- layers_by_source: LAYERS_BY_SOURCE
-};
+// export singleton
+module.exports = tm;
\ No newline at end of file
diff --git a/middleware/geocodeJSON.js b/middleware/geocodeJSON.js
index 0ff2e303..60c74f68 100644
--- a/middleware/geocodeJSON.js
+++ b/middleware/geocodeJSON.js
@@ -47,10 +47,10 @@ function convertToGeocodeJSON(req, res, next, opts) {
// OPTIONAL. Default: null. The attribution of the data. In case of multiple sources,
// and then multiple attributions, can be an object with one key by source.
// Can be a URI on the server, which outlines attribution details.
- res.body.geocoding.attribution = url.format({
+ res.body.geocoding.attribution = opts.config.attributionURL || url.format({
protocol: req.protocol,
host: req.get('host'),
- pathname: opts.basePath + 'attribution'
+ pathname: 'attribution'
});
// OPTIONAL. Default: null. The query that has been issued to trigger the
diff --git a/middleware/interpolate.js b/middleware/interpolate.js
index 10c3f7e8..8507709a 100644
--- a/middleware/interpolate.js
+++ b/middleware/interpolate.js
@@ -2,6 +2,7 @@ const async = require('async');
const logger = require( 'pelias-logger' ).get( 'api' );
const source_mapping = require('../helper/type_mapping').source_mapping;
const _ = require('lodash');
+const stable = require('stable');
/**
example response from interpolation web service:
@@ -116,7 +117,7 @@ function setup(service, should_execute) {
// sort the results to ensure that addresses show up higher than street centroids
if (_.has(res, 'data')) {
- res.data.sort((a, b) => {
+ res.data = stable(res.data, (a, b) => {
if (a.layer === 'address' && b.layer !== 'address') { return -1; }
if (a.layer !== 'address' && b.layer === 'address') { return 1; }
return 0;
diff --git a/package.json b/package.json
index 34af521a..2f361801 100644
--- a/package.json
+++ b/package.json
@@ -36,32 +36,33 @@
"node": ">=6.0.0"
},
"dependencies": {
+ "@mapbox/geojson-extent": "^0.3.1",
"addressit": "1.5.0",
"async": "^2.0.0",
"check-types": "^7.0.0",
- "elasticsearch": "^14.2.1",
+ "elasticsearch": "^15.0.0",
"elasticsearch-exceptions": "0.0.4",
"express": "^4.8.8",
"geojson": "^0.5.0",
- "@mapbox/geojson-extent": "^0.3.1",
"geolib": "^2.0.18",
"iso-639-3": "^1.0.0",
"iso3166-1": "^0.3.0",
- "joi": "^12.0.0",
+ "joi": "^13.1.3",
"locale": "^0.1.0",
"lodash": "^4.17.4",
"markdown": "0.5.0",
"morgan": "^1.8.2",
"pelias-categories": "1.2.0",
- "pelias-config": "2.14.0",
+ "pelias-config": "3.0.2",
"pelias-labels": "1.8.0",
- "pelias-logger": "0.3.1",
- "pelias-microservice-wrapper": "1.3.0",
- "pelias-model": "5.3.2",
+ "pelias-logger": "0.4.2",
+ "pelias-microservice-wrapper": "1.4.0",
+ "pelias-model": "5.5.2",
"pelias-query": "9.1.1",
- "pelias-sorting": "1.1.0",
+ "pelias-sorting": "1.2.0",
"predicates": "^2.0.0",
- "retry": "^0.10.1",
+ "retry": "^0.12.0",
+ "stable": "^0.1.8",
"stats-lite": "^2.0.4",
"through2": "^2.0.3"
},
@@ -71,7 +72,7 @@
"istanbul": "^0.4.2",
"jshint": "^2.5.6",
"nsp": "^3.0.0",
- "pelias-mock-logger": "1.2.0",
+ "pelias-mock-logger": "1.3.0",
"precommit-hook": "^3.0.0",
"proxyquire": "^2.0.0",
"semantic-release": "^15.1.4",
@@ -88,6 +89,7 @@
"test"
],
"release": {
- "branch": "production"
+ "branch": "production",
+ "success": []
}
}
diff --git a/test/unit/helper/TypeMapping.js b/test/unit/helper/TypeMapping.js
new file mode 100644
index 00000000..b4e45475
--- /dev/null
+++ b/test/unit/helper/TypeMapping.js
@@ -0,0 +1,208 @@
+const _ = require('lodash');
+const TypeMapping = require('../../../helper/TypeMapping');
+
+module.exports.tests = {};
+
+module.exports.tests.interface = function(test) {
+ test('valid interface', function(t) {
+ t.equal(typeof TypeMapping, 'function', 'TypeMapping is a function');
+
+ t.equal(typeof TypeMapping.addStandardTargetsToAliases, 'function', 'addStandardTargetsToAliases() is a function');
+ t.equal(typeof TypeMapping.prototype.setSourceAliases, 'function', 'setSourceAliases() is a function');
+
+ t.equal(typeof TypeMapping.prototype.setLayersBySource, 'function', 'setLayersBySource() is a function');
+ t.equal(typeof TypeMapping.prototype.setLayerAliases, 'function', 'setLayerAliases() is a function');
+
+ t.equal(typeof TypeMapping.prototype.generateMappings, 'function', 'generateMappings() is a function');
+
+ t.equal(typeof TypeMapping.prototype.loadTargets, 'function', 'loadTargets() is a function');
+ t.equal(typeof TypeMapping.prototype.load, 'function', 'load() is a function');
+
+ t.end();
+ });
+};
+
+module.exports.tests.constructor = function(test) {
+ test('constructor', function(t) {
+
+ var doc = new TypeMapping();
+
+ // initial values
+ t.deepEqual(doc.sources, [], 'initial value');
+ t.deepEqual(doc.source_aliases, {}, 'initial value');
+ t.deepEqual(doc.layers, [], 'initial value');
+ t.deepEqual(doc.layers_by_source, {}, 'initial value');
+ t.deepEqual(doc.layer_aliases, {}, 'initial value');
+ t.deepEqual(doc.source_mapping, {}, 'initial value');
+ t.deepEqual(doc.layer_mapping, {}, 'initial value');
+
+ t.end();
+ });
+};
+
+module.exports.tests.addStandardTargetsToAliases = function(test) {
+ test('static method addStandardTargetsToAliases', function(t) {
+
+ var aliases = { test: ['test2'] };
+
+ t.deepEqual(
+ TypeMapping.addStandardTargetsToAliases([], aliases),
+ { test: ['test2'] }
+ );
+ t.deepEqual(aliases, aliases, 'aliases object not mutated');
+
+ t.deepEqual(
+ TypeMapping.addStandardTargetsToAliases(['test'], aliases),
+ { test: ['test2'] },
+ 'not modified'
+ );
+ t.deepEqual(aliases, aliases, 'aliases object not mutated');
+
+ t.deepEqual(
+ TypeMapping.addStandardTargetsToAliases(['baz'], aliases),
+ { test: ['test2'], baz: ['baz'] }
+ );
+ t.deepEqual(aliases, aliases, 'aliases object not mutated');
+
+ t.deepEqual(
+ TypeMapping.addStandardTargetsToAliases(['baz','boo'], aliases),
+ { test: ['test2'], baz: ['baz'], boo: ['boo'] }
+ );
+ t.deepEqual(aliases, aliases, 'aliases object not mutated');
+
+ t.end();
+
+ });
+};
+
+module.exports.tests.setSourceAliases = function(test) {
+ test('setter setSourceAliases', function(t) {
+ var tm = new TypeMapping();
+ t.deepEqual(tm.source_aliases, {});
+ tm.setSourceAliases({ foo: ['foo', 'bar'] });
+ t.deepEqual(tm.source_aliases, { foo: ['foo', 'bar'] });
+ t.end();
+ });
+};
+
+module.exports.tests.setLayersBySource = function(test) {
+ test('setter setLayersBySource', function(t) {
+ var tm = new TypeMapping();
+ t.deepEqual(tm.layers_by_source, {});
+ tm.setLayersBySource({ foo: ['foo', 'bar'] });
+ t.deepEqual(tm.layers_by_source, { foo: ['foo', 'bar'] });
+ t.end();
+ });
+};
+
+module.exports.tests.setLayerAliases = function(test) {
+ test('setter setLayerAliases', function(t) {
+ var tm = new TypeMapping();
+ t.deepEqual(tm.layer_aliases, {});
+ tm.setLayerAliases({ foo: ['foo', 'bar'] });
+ t.deepEqual(tm.layer_aliases, { foo: ['foo', 'bar'] });
+ t.end();
+ });
+};
+
+module.exports.tests.generateMappings = function(test) {
+ test('generateMappings - no-op', function(t) {
+ var tm = new TypeMapping();
+ t.deepEqual(tm.sources, []);
+ t.deepEqual(tm.source_mapping, {});
+ t.deepEqual(tm.layers, []);
+ t.deepEqual(tm.layer_mapping, {});
+ tm.generateMappings();
+ t.deepEqual(tm.sources, []);
+ t.deepEqual(tm.source_mapping, {});
+ t.deepEqual(tm.layers, []);
+ t.deepEqual(tm.layer_mapping, {});
+ t.end();
+ });
+ test('generateMappings - sources', function(t) {
+ var tm = new TypeMapping();
+ tm.layers_by_source = { foo: ['foo'], faz: ['faz'] };
+ tm.generateMappings();
+ t.deepEqual(tm.sources, ['foo', 'faz']);
+ t.end();
+ });
+ test('generateMappings - source_mapping', function(t) {
+ var tm = new TypeMapping();
+ tm.layers_by_source = { foo: ['foo'], faz: ['faz'] };
+ tm.source_aliases = { foo: ['foo','f'], bar: ['bar', 'b'], baz: ['baz'] };
+ tm.generateMappings();
+ t.deepEqual(tm.source_mapping, { foo: ['foo', 'f'], bar: ['bar', 'b'], baz: ['baz'], faz: ['faz'] });
+ t.end();
+ });
+ test('generateMappings - layers', function(t) {
+ var tm = new TypeMapping();
+ tm.layers_by_source = { foo: ['foo'], faz: ['faz'] };
+ tm.generateMappings();
+ t.deepEqual(tm.layers, ['foo','faz']);
+ t.end();
+ });
+ test('generateMappings - layer_mapping', function(t) {
+ var tm = new TypeMapping();
+ tm.layers_by_source = { foo: ['foo'], faz: ['faz'] };
+ tm.layer_aliases = { foo: ['foo','f'], bar: ['bar', 'b'], baz: ['baz'] };
+ tm.generateMappings();
+ t.deepEqual(tm.layer_mapping, { foo: ['foo', 'f'], bar: ['bar', 'b'], baz: ['baz'], faz: ['faz'] });
+ t.end();
+ });
+};
+
+module.exports.tests.loadTargets = function(test) {
+ test('loadTargets - undefined', function(t) {
+ var tm = new TypeMapping();
+ tm.loadTargets();
+ t.deepEqual(tm.sources, []);
+ t.deepEqual(tm.source_mapping, {});
+ t.deepEqual(tm.layers, []);
+ t.deepEqual(tm.layer_mapping, {});
+ t.end();
+ });
+ test('loadTargets', function(t) {
+ var tm = new TypeMapping();
+ tm.loadTargets({
+ source_aliases: { source1: ['s1', 's2'], source2: ['s3', 's4'] },
+ layers_by_source: { source1: ['layer1', 'layer3'], source2: ['layer2'] },
+ layer_aliases: { layer1: ['l1', 'l2'], layer2: ['l3', 'l4'] },
+ });
+ t.deepEqual(tm.sources, [ 'source1', 'source2' ]);
+ t.deepEqual(tm.source_mapping, { source1: [ 's1', 's2' ], source2: [ 's3', 's4' ] });
+ t.deepEqual(tm.layers, [ 'layer1', 'layer3', 'layer2' ]);
+ t.deepEqual(tm.layer_mapping, { layer1: [ 'l1', 'l2' ], layer2: [ 'l3', 'l4' ], layer3: [ 'layer3' ] });
+ t.end();
+ });
+};
+
+module.exports.tests.load = function(test) {
+ test('load from pelias config', function(t) {
+ var tm = new TypeMapping();
+ tm.load(() => {
+
+ // load pelias config
+ const expected = _.get(
+ require('pelias-config').generate(require('../../../schema')),
+ 'api.targets', {}
+ );
+
+ // values copied from config
+ t.deepEqual(tm.layers_by_source, expected.layers_by_source || {});
+ t.deepEqual(tm.source_aliases, expected.source_aliases || {});
+ t.deepEqual(tm.layer_aliases, expected.layer_aliases || {});
+ t.end();
+ });
+ });
+};
+
+module.exports.all = function (tape, common) {
+
+ function test(name, testFunction) {
+ return tape('TypeMapping: ' + name, testFunction);
+ }
+
+ for( var testCase in module.exports.tests ){
+ module.exports.tests[testCase](test, common);
+ }
+};
diff --git a/test/unit/helper/type_mapping.js b/test/unit/helper/type_mapping.js
index ec8cd379..25b06d0f 100644
--- a/test/unit/helper/type_mapping.js
+++ b/test/unit/helper/type_mapping.js
@@ -4,18 +4,31 @@ var type_mapping = require('../../../helper/type_mapping');
module.exports.tests = {};
module.exports.tests.interfaces = function(test, common) {
- test('basic layer mapping', function(t) {
- t.deepEquals(type_mapping.layer_mapping.venue, ['venue']);
- t.deepEquals(type_mapping.layer_mapping.address, ['address']);
+
+ test('complete sources', function(t) {
+ t.deepEquals(type_mapping.sources, [ 'openstreetmap', 'openaddresses', 'geonames', 'whosonfirst' ]);
t.end();
});
- test('alias layer mapping', function(t) {
- t.deepEquals(type_mapping.layer_mapping.coarse,
- [ 'continent', 'empire', 'country', 'dependency', 'macroregion',
- 'region', 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood',
- 'borough', 'neighbourhood', 'microhood', 'disputed', 'postalcode',
- 'continent', 'ocean', 'marinearea']);
+ test('complete layers', function(t) {
+ t.deepEquals(type_mapping.layers, [ 'address', 'venue', 'street', 'country', 'macroregion',
+ 'region', 'county', 'localadmin', 'locality', 'borough', 'neighbourhood', 'continent',
+ 'empire', 'dependency', 'macrocounty', 'macrohood', 'microhood', 'disputed',
+ 'postalcode', 'ocean', 'marinearea' ]);
+ t.end();
+ });
+
+ test('complete source mapping', function(t) {
+ t.deepEquals(type_mapping.source_mapping, {
+ osm: [ 'openstreetmap' ],
+ oa: [ 'openaddresses' ],
+ gn: [ 'geonames' ],
+ wof: [ 'whosonfirst' ],
+ openstreetmap: [ 'openstreetmap' ],
+ openaddresses: [ 'openaddresses' ],
+ geonames: [ 'geonames' ],
+ whosonfirst: [ 'whosonfirst' ]
+ });
t.end();
});
@@ -30,6 +43,66 @@ module.exports.tests.interfaces = function(test, common) {
t.deepEquals(type_mapping.source_mapping.wof, ['whosonfirst']);
t.end();
});
+
+ test('complete layer mapping', function(t) {
+ t.deepEquals(type_mapping.layer_mapping, {
+ coarse: [ 'continent', 'empire', 'country', 'dependency', 'macroregion', 'region',
+ 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood', 'borough',
+ 'neighbourhood', 'microhood', 'disputed', 'postalcode', 'continent', 'ocean', 'marinearea'
+ ],
+ address: [ 'address' ],
+ venue: [ 'venue' ],
+ street: [ 'street' ],
+ country: [ 'country' ],
+ macroregion: [ 'macroregion' ],
+ region: [ 'region' ],
+ county: [ 'county' ],
+ localadmin: [ 'localadmin' ],
+ locality: [ 'locality' ],
+ borough: [ 'borough' ],
+ neighbourhood: [ 'neighbourhood' ],
+ continent: [ 'continent' ],
+ empire: [ 'empire' ],
+ dependency: [ 'dependency' ],
+ macrocounty: [ 'macrocounty' ],
+ macrohood: [ 'macrohood' ],
+ microhood: [ 'microhood' ],
+ disputed: [ 'disputed' ],
+ postalcode: [ 'postalcode' ],
+ ocean: [ 'ocean' ],
+ marinearea: [ 'marinearea' ]
+ });
+ t.end();
+ });
+
+ test('basic layer mapping', function(t) {
+ t.deepEquals(type_mapping.layer_mapping.venue, ['venue']);
+ t.deepEquals(type_mapping.layer_mapping.address, ['address']);
+ t.end();
+ });
+
+ test('alias layer mapping', function(t) {
+ t.deepEquals(type_mapping.layer_mapping.coarse,
+ [ 'continent', 'empire', 'country', 'dependency', 'macroregion',
+ 'region', 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood',
+ 'borough', 'neighbourhood', 'microhood', 'disputed', 'postalcode',
+ 'continent', 'ocean', 'marinearea']);
+ t.end();
+ });
+
+ test('complete layers by source', function(t) {
+ t.deepEquals(type_mapping.layers_by_source, {
+ openstreetmap: [ 'address', 'venue', 'street' ],
+ openaddresses: [ 'address' ],
+ geonames: [ 'country', 'macroregion', 'region', 'county', 'localadmin',
+ 'locality', 'borough', 'neighbourhood', 'venue' ],
+ whosonfirst: [ 'continent', 'empire', 'country', 'dependency', 'macroregion',
+ 'region', 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood',
+ 'borough', 'neighbourhood', 'microhood', 'disputed', 'venue', 'postalcode',
+ 'continent', 'ocean', 'marinearea' ]
+ });
+ t.end();
+ });
};
module.exports.all = function (tape, common) {
diff --git a/test/unit/middleware/interpolate.js b/test/unit/middleware/interpolate.js
index 3602592c..537c7444 100644
--- a/test/unit/middleware/interpolate.js
+++ b/test/unit/middleware/interpolate.js
@@ -277,6 +277,81 @@ module.exports.tests.success_conditions = (test, common) => {
});
+ test('results should be sorted first by address/non-address. previous ordering should otherwise be maintained via a stable sort', t => {
+ const service = (req, res, callback) => {
+ // results 5 and 7 will have interpolated results returned
+ // this is to ensure results are re-sorted to move the addresses first
+ if (res.id === 5 || res.id === 7) {
+ callback(null, {
+ properties: {
+ number: 17,
+ source: 'Source Abbr 1',
+ source_id: 'source 1 source id',
+ lat: 12.121212,
+ lon: 21.212121
+ }
+ });
+ } else {
+ // return empty results in most cases
+ callback(null, {});
+ }
+ };
+
+ const logger = require('pelias-mock-logger')();
+
+ const controller = proxyquire('../../../middleware/interpolate', {
+ 'pelias-logger': logger
+ })(service, () => true);
+
+ const req = {
+ clean: {
+ parsed_text: 'this is req.clean.parsed_text'
+ }
+ };
+ const res = {};
+
+ // helper method to generate test results which default to streets
+ function generateTestStreets(id) {
+ return {
+ id: id+1,
+ layer: 'street',
+ name: { default: `name ${id+1}` },
+ address_parts: {},
+ source_id: 'original source_id'
+ };
+ }
+
+ // generate a set of street results of desired size
+ // NOTE: this set must be of 11 elements or greater
+ // Node.js uses stable insertion sort for arrays of 10 or fewer elements,
+ // but _unstable_ QuickSort for larger arrays
+ const resultCount = 11;
+ const sequence_array = Array.from(new Array(resultCount),(val,index)=>index);
+ res.data = sequence_array.map(generateTestStreets);
+
+ controller(req, res, () => {
+ t.notOk(logger.hasErrorMessages(), 'there shouldn\'t be any error messages');
+
+ const results = res.data;
+
+ t.equals(results.length, results.length, 'correct number of results should be returned');
+ t.equals(results[0].layer, 'address', 'first result should be interpolated address');
+ t.equals(results[1].layer, 'address', 'second result should be interpolated address');
+
+ // iterate through all remaining records, ensuring their ids are increasing,
+ // as was the case when the set of streets was originally generated
+ let previous_id;
+ for (let i = 2; i < results.length; i++) {
+ if (previous_id) {
+ t.ok(results[i].id > previous_id, `id ${results[i].id} should be higher than ${previous_id}, to ensure sort is stable`);
+ }
+ previous_id = results[i].id;
+ }
+
+ t.end();
+ });
+ });
+
test('service call returning error should not map in interpolated results for non-errors', t => {
const service = (req, res, callback) => {
if (res.id === 1) {
diff --git a/test/unit/run.js b/test/unit/run.js
index 52b6e519..404cd86b 100644
--- a/test/unit/run.js
+++ b/test/unit/run.js
@@ -36,6 +36,7 @@ var tests = [
require('./helper/geojsonify_place_details'),
require('./helper/geojsonify'),
require('./helper/logging'),
+ require('./helper/TypeMapping'),
require('./helper/type_mapping'),
require('./helper/stackTraceLine'),
require('./middleware/access_log'),