pelias-openstreetmap 5.13.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/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "pelias-openstreetmap",
3
+ "version": "5.13.0",
4
+ "engines": {
5
+ "node": ">=10.0.0"
6
+ },
7
+ "author": "pelias",
8
+ "description": "Pelias openstreetmap utilities",
9
+ "homepage": "https://github.com/mapzen/pelias-openstreetmap",
10
+ "license": "MIT",
11
+ "main": "index.js",
12
+ "scripts": {
13
+ "download": "./bin/download",
14
+ "coverage": "node_modules/.bin/istanbul cover test/run.js",
15
+ "end-to-end": "export NODE_ENV=test && node test/end-to-end.js;",
16
+ "lint": "jshint .",
17
+ "start": "./bin/start",
18
+ "test": "npm run units",
19
+ "ci": "node ./test/ci-config.js && npm test && npm run end-to-end",
20
+ "units": "./bin/test",
21
+ "validate": "npm ls"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/mapzen/pelias-openstreetmap.git"
26
+ },
27
+ "keywords": [
28
+ "pelias",
29
+ "stream",
30
+ "openstreetmap",
31
+ "elasticsearch"
32
+ ],
33
+ "bugs": {
34
+ "url": "https://github.com/mapzen/pelias-openstreetmap/issues"
35
+ },
36
+ "dependencies": {
37
+ "async": "^3.0.1",
38
+ "combined-stream": "^1.0.5",
39
+ "extend": "^3.0.0",
40
+ "is-object": "^1.0.1",
41
+ "iso-639-3": "^1.0.0",
42
+ "joi": "^14.0.0",
43
+ "lodash": "^4.17.4",
44
+ "pbf2json": "^6.9.0",
45
+ "pelias-blacklist-stream": "^1.0.0",
46
+ "pelias-config": "^4.12.0",
47
+ "pelias-dbclient": "^2.13.0",
48
+ "pelias-logger": "^1.2.1",
49
+ "pelias-model": "^9.1.1",
50
+ "pelias-wof-admin-lookup": "^7.4.1",
51
+ "through2": "^3.0.0",
52
+ "through2-sink": "^1.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "colors": "^1.1.2",
56
+ "deep-diff": "^1.0.0",
57
+ "istanbul": "^0.4.3",
58
+ "jshint": "^2.10.3",
59
+ "naivedb": "^1.0.7",
60
+ "pelias-mock-logger": "^1.0.1",
61
+ "precommit-hook": "^3.0.0",
62
+ "proxyquire": "^2.0.0",
63
+ "stream-mock": "^2.0.3",
64
+ "taginfo": "^1.0.1",
65
+ "tap-spec": "^5.0.0",
66
+ "tape": "^5.0.0"
67
+ },
68
+ "pre-commit": [
69
+ "lint",
70
+ "validate",
71
+ "test"
72
+ ],
73
+ "release": {
74
+ "branch": "master",
75
+ "success": []
76
+ }
77
+ }
@@ -0,0 +1,25 @@
1
+
2
+ /**
3
+ Attempt to map OSM address tags to the Pelias address schema.
4
+
5
+ On the left is the OSM tag name, on the right is corresponding
6
+ doc.address_parts key for which it should be mapped to.
7
+
8
+ eg. tags['addr:street'] -> doc.address_parts['street']
9
+
10
+ @ref: http://wiki.openstreetmap.org/wiki/Karlsruhe_Schema
11
+ **/
12
+
13
+ var KARLSRUHE_SCHEMA = {
14
+ 'addr:housename': 'name',
15
+ 'addr:housenumber': 'number',
16
+ 'addr:street': 'street',
17
+ 'addr:postcode': 'zip'
18
+
19
+ // @ref: https://github.com/pelias/model/pull/13
20
+ // 'addr:state': 'state',
21
+ // 'addr:city': 'city',
22
+ // 'addr:country': 'country'
23
+ };
24
+
25
+ module.exports = KARLSRUHE_SCHEMA;
@@ -0,0 +1,17 @@
1
+
2
+ /**
3
+ Attempt to map OSM address tags to the Pelias address schema.
4
+
5
+ On the left is the OSM tag name, on the right is corresponding
6
+ doc.address key for which it should be mapped to.
7
+
8
+ eg. tags['naptan:Street'] -> doc.address_parts['street']
9
+
10
+ @ref: http://wiki.openstreetmap.org/wiki/NaPTAN
11
+ **/
12
+
13
+ var NAPTAN_SCHEMA = {
14
+ 'naptan:Street': 'street'
15
+ };
16
+
17
+ module.exports = NAPTAN_SCHEMA;
@@ -0,0 +1,17 @@
1
+
2
+ /**
3
+ Attempt to map OSM address tags to the Pelias address schema.
4
+
5
+ On the left is the OSM tag name, on the right is corresponding
6
+ doc.address key for which it should be mapped to.
7
+
8
+ eg. tags['postal_code'] -> doc.address_parts['zip']
9
+
10
+ @ref: http://wiki.openstreetmap.org/wiki/Key:postal_code
11
+ **/
12
+
13
+ var OSM_SCHEMA = {
14
+ 'postal_code': 'zip'
15
+ };
16
+
17
+ module.exports = OSM_SCHEMA;
@@ -0,0 +1,18 @@
1
+
2
+ /**
3
+ Attempt to map OSM TIGER tags to the Pelias address schema.
4
+
5
+ On the left is the OSM tag name, on the right is corresponding
6
+ doc.address key for which it should be mapped to.
7
+
8
+ eg. tags['tiger:zip_left'] -> doc.address_parts['zip']
9
+
10
+ @ref: http://wiki.openstreetmap.org/wiki/TIGER_to_OSM_Attribute_Map
11
+ **/
12
+
13
+ var TIGER_SCHEMA = {
14
+ 'tiger:zip_left': 'zip',
15
+ 'tiger:zip_right': 'zip'
16
+ };
17
+
18
+ module.exports = TIGER_SCHEMA;
@@ -0,0 +1,51 @@
1
+
2
+ /**
3
+ Attempt to map OSM name tags to the Pelias naming schema.
4
+
5
+ On the left is the OSM tag name, on the right is corresponding
6
+ doc.name key for which it should be mapped to.
7
+
8
+ eg. tags['alt_name'] -> doc.name['alternative']
9
+
10
+ The value 'default' is special, it defines the name used by default
11
+ for elasticsearch queries, it has the unique ability to specify multiple
12
+ keys with a value of 'default'.
13
+
14
+ When multiple keys have the value 'default' then they are considered
15
+ as aliases of the default field.
16
+
17
+ The '_primary' property defined below defines which of those aliases
18
+ is considered the 'primary default name' for label generation.
19
+
20
+ No values other than 'default' should be specified more than once.
21
+
22
+ @ref: http://wiki.openstreetmap.org/wiki/Key:name
23
+ @ref: http://wiki.openstreetmap.org/wiki/Names
24
+ **/
25
+
26
+ var OSM_NAMING_SCHEMA = {
27
+ 'name': 'default',
28
+ 'loc_name': 'default',
29
+ 'alt_name': 'default',
30
+ 'short_name': 'default',
31
+
32
+ // note: these aliases are currently disabled because they are not being used when querying
33
+ // 'int_name': 'international',
34
+ // 'nat_name': 'national',
35
+ // 'official_name': 'official',
36
+ // 'old_name': 'old',
37
+ // 'reg_name': 'regional',
38
+ // 'sorting_name': 'sorting'
39
+ };
40
+
41
+ // this property is considered the 'primary name'
42
+ // for label generation, the others are considered
43
+ // secondary or 'aliases'.
44
+ Object.defineProperty(OSM_NAMING_SCHEMA, '_primary', {
45
+ value: 'name',
46
+ enumerable: false,
47
+ configurable: false,
48
+ writable: false
49
+ });
50
+
51
+ module.exports = OSM_NAMING_SCHEMA;
package/schema.js ADDED
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const Joi = require('joi');
4
+
5
+ // Schema Configuration
6
+ // datapath: string (required)
7
+ // leveldbpath: string (required)
8
+ // import: array of objects containing filename (optional)
9
+ // importVenues: boolean (optional)
10
+ // download: array of objects containing sourceURL (optional)
11
+ // deduplicate: boolean (optional)
12
+ module.exports = Joi.object().keys({
13
+ imports: Joi.object().keys({
14
+ openstreetmap: Joi.object().keys({
15
+ datapath: Joi.string(),
16
+ leveldbpath: Joi.string(),
17
+ import: Joi.array().items(Joi.object().keys({
18
+ filename: Joi.string(),
19
+ importVenues: Joi.boolean().default(true).truthy('yes').falsy('no').insensitive(true)
20
+ }).requiredKeys('filename').unknown(true)),
21
+ download: Joi.array().items(Joi.object().keys({
22
+ sourceURL: Joi.string()
23
+ }).requiredKeys('sourceURL').unknown(true)),
24
+ deduplicate: Joi.boolean()
25
+ }).requiredKeys('datapath', 'leveldbpath', 'import').unknown(true)
26
+ }).requiredKeys('openstreetmap').unknown(true)
27
+ }).requiredKeys('imports').unknown(true);
@@ -0,0 +1,71 @@
1
+ /**
2
+ The addendum mapper is responsible for adding interesting metadata
3
+ as an 'addendum' to the record.
4
+
5
+ @see: https://github.com/pelias/api/pull/1255
6
+ **/
7
+
8
+ const through = require('through2');
9
+ const peliasLogger = require('pelias-logger').get('openstreetmap');
10
+ const whitelist = [
11
+ 'wheelchair', // Wheelchair accessibility
12
+ 'iata', // IATA airport codes
13
+ 'icao', // ICAO airport codes
14
+ 'wikidata', // Wikidata concordance
15
+ 'wikipedia', // Wikipedia concordance
16
+ 'operator', // Operator name
17
+ 'brand', // Brand name
18
+ 'website', // Website URL
19
+ 'phone', // Telephone number
20
+ 'opening_hours', // Opening hours
21
+
22
+ // COVID-19
23
+ 'opening_hours:covid19',
24
+ 'delivery:covid19',
25
+ 'safety:mask:covid19'
26
+ ];
27
+
28
+ module.exports = function(){
29
+
30
+ return through.obj(( doc, enc, next ) => {
31
+
32
+ try {
33
+
34
+ // skip records with no tags
35
+ let tags = doc.getMeta('tags');
36
+ if( !tags ){
37
+ return next( null, doc );
38
+ }
39
+
40
+ let addendum = {};
41
+
42
+ // iterate over mapping
43
+ whitelist.forEach(key => {
44
+
45
+ // check each whitelist key against document tags
46
+ if( !tags.hasOwnProperty( key ) ){ return; }
47
+
48
+ // set addendum key
49
+ let value = tags[key].trim();
50
+ if( !!value.length ){
51
+ addendum[ key ] = value;
52
+ }
53
+ });
54
+
55
+ // set document addendum
56
+ if( !!Object.keys( addendum ).length ){
57
+ doc.setAddendum( 'osm', addendum );
58
+ }
59
+ }
60
+
61
+ catch( e ){
62
+ peliasLogger.error( 'addendum_mapper error' );
63
+ peliasLogger.error( e.stack );
64
+ peliasLogger.error( JSON.stringify( doc, null, 2 ) );
65
+ }
66
+
67
+ return next( null, doc );
68
+
69
+ });
70
+
71
+ };
@@ -0,0 +1,131 @@
1
+
2
+ /**
3
+ The address extractor is responsible for cloning documents where a valid address
4
+ data exists.
5
+
6
+ The hasValidAddress() function examines the address property which was populated
7
+ earier in the pipeline by the osm.tag.mapper stream and therefore MUST come after
8
+ that stream in the pipeline or it will fail to find any address information.
9
+
10
+ There are a few different outcomes for this stream depending on the data contained
11
+ in each individual document, the result can be, 0, 1 or 2 documents being emitted.
12
+
13
+ In the case of the document missing a valid doc.name.default string then it is not
14
+ considered to be a point-of-interest in it's own right, it will be discarded.
15
+
16
+ In the case where the document contains BOTH a valid house number & street name we
17
+ consider this record to be an address in it's own right and we clone that record,
18
+ duplicating the data across to the new doc instance while adjusting it's id and layer.
19
+
20
+ In a rare case it is possible that the record contains neither a valid name nor a valid
21
+ address. If this case in encountered then the parser should be modified so these records
22
+ are no longer passed down the pipeline; as they will simply be discarded because they are
23
+ not searchable.
24
+ **/
25
+
26
+ var through = require('through2');
27
+ var isObject = require('is-object');
28
+ var extend = require('extend');
29
+ var peliasLogger = require( 'pelias-logger' ).get( 'openstreetmap' );
30
+ var Document = require('pelias-model').Document;
31
+
32
+ function hasValidAddress( doc ){
33
+ if( !isObject( doc ) ){ return false; }
34
+ if( !isObject( doc.address_parts ) ){ return false; }
35
+ if( 'string' !== typeof doc.address_parts.number ){ return false; }
36
+ if( 'string' !== typeof doc.address_parts.street ){ return false; }
37
+ if( !doc.address_parts.number.length ){ return false; }
38
+ if( !doc.address_parts.street.length ){ return false; }
39
+ return true;
40
+ }
41
+
42
+ module.exports = function(){
43
+
44
+ var stream = through.obj( function( doc, enc, next ) {
45
+ var isNamedPoi = !!doc.getName('default');
46
+ var isAddress = hasValidAddress( doc );
47
+
48
+ // accept semi-colon delimited house numbers
49
+ // ref: https://github.com/pelias/openstreetmap/issues/21
50
+ var streetNumbers = (doc.getAddress('number') || '').split(';').map(Function.prototype.call, String.prototype.trim);
51
+
52
+ // create a new record for street addresses
53
+ if( isAddress ){
54
+ var record;
55
+
56
+ streetNumbers.forEach( function( streetno, i ){
57
+
58
+ try {
59
+ var newid = [ doc.getSourceId() ];
60
+ if( i > 0 ){
61
+ newid.push( i );
62
+ peliasLogger.debug('[address_extractor] found multiple house numbers: ', streetNumbers);
63
+ }
64
+
65
+ // copy data to new document
66
+ record = new Document( 'openstreetmap', 'address', newid.join('/') )
67
+ .setName( 'default', streetno + ' ' + doc.address_parts.street )
68
+ .setCentroid( doc.getCentroid() );
69
+
70
+ // copy all address properties
71
+ setProperties( record, doc );
72
+
73
+ // ensure that only a single address number is used
74
+ record.setAddress('number', streetno);
75
+ }
76
+
77
+ catch( e ){
78
+ peliasLogger.error( 'address_extractor error' );
79
+ peliasLogger.error( e.stack );
80
+ peliasLogger.error( JSON.stringify( doc, null, 2 ) );
81
+ }
82
+
83
+ if( record !== undefined ){
84
+ // copy meta data (but maintain the id assigned above)
85
+ record._meta = extend( true, {}, doc._meta, { id: record.getId() } );
86
+ this.push( record );
87
+ }
88
+ else {
89
+ peliasLogger.error( '[address_extractor] failed to push address downstream' );
90
+ }
91
+
92
+ }, this);
93
+
94
+ }
95
+
96
+ // forward doc downstream if it's a POI in its own right
97
+ // note: this MUST be below the address push()
98
+ if( isNamedPoi ){
99
+
100
+ // in the case of multiple house numbers, only use the first one for the venue
101
+ if (streetNumbers.length > 1) {
102
+ doc.setAddress('number', streetNumbers[0]);
103
+ }
104
+
105
+ this.push( doc );
106
+ }
107
+
108
+ return next();
109
+
110
+ });
111
+
112
+ // catch stream errors
113
+ stream.on( 'error', peliasLogger.error.bind( peliasLogger, __filename ) );
114
+
115
+ return stream;
116
+ };
117
+
118
+ // properties to map from the osm record to the pelias doc
119
+ var addrProps = [ 'name', 'number', 'street', 'zip' ];
120
+
121
+ // call document setters and ignore non-fatal errors
122
+ function setProperties( record, doc ){
123
+ addrProps.forEach( function ( prop ){
124
+ try {
125
+ record.setAddress( prop, doc.getAddress( prop ) );
126
+ } catch ( ex ) {}
127
+ });
128
+ }
129
+
130
+ // export for testing
131
+ module.exports.hasValidAddress = hasValidAddress;
@@ -0,0 +1,77 @@
1
+
2
+ /**
3
+ The category mapper is responsible for checking each documents tags
4
+ against a predefined taxonomy mapping file.
5
+
6
+ For each openstreetmap 'feature' that matches the mapping file we
7
+ create a new document 'category' by calling `doc.addCategory( category );`.
8
+
9
+ This idea is to classify places-of-interest in order to facilitate the
10
+ building of industry and interest specific geocoders.
11
+
12
+ @see: https://github.com/pelias/pelias/wiki/Taxonomy-v1
13
+ @see: ./config/category_map.js
14
+ **/
15
+
16
+ const _ = require('lodash');
17
+ const through = require('through2');
18
+ const peliasLogger = require('pelias-logger').get('openstreetmap');
19
+
20
+ module.exports = function( mapping ){
21
+
22
+ return through.obj( function( doc, enc, next ){
23
+
24
+ try {
25
+
26
+ // do not categorize addresses
27
+ if( doc.getLayer().match('address') ){
28
+ return next( null, doc );
29
+ }
30
+
31
+ // skip records with no tags
32
+ var tags = doc.getMeta('tags');
33
+ if( !tags ){
34
+ return next( null, doc );
35
+ }
36
+
37
+ // iterate over mapping
38
+ _.each(mapping, (osmTagValues, osmTagKey) => {
39
+
40
+ // check each mapping key against document tags
41
+ if( !tags.hasOwnProperty( osmTagKey ) ){ return; }
42
+
43
+ // handle mapping wildcards
44
+ if( osmTagValues.hasOwnProperty('*') ){
45
+ addCategories( doc, osmTagValues['*'] );
46
+ }
47
+
48
+ // handle regular features
49
+ _.each(osmTagValues, (categories, osmTagValue) => {
50
+ if( '*' === osmTagValue ){ return; }
51
+ if( tags[osmTagKey] === osmTagValue ){
52
+ addCategories( doc, categories );
53
+ }
54
+ });
55
+ });
56
+ }
57
+
58
+ catch( e ){
59
+ peliasLogger.error( 'category_mapper error' );
60
+ peliasLogger.error( e.stack );
61
+ peliasLogger.error( JSON.stringify( doc, null, 2 ) );
62
+ }
63
+
64
+ return next( null, doc );
65
+
66
+ });
67
+
68
+ };
69
+
70
+ function addCategories( doc, categories ){
71
+ if( !Array.isArray( categories ) ){
72
+ categories = [categories];
73
+ }
74
+ categories.forEach( function( category ){
75
+ doc.addCategory( category );
76
+ });
77
+ }
@@ -0,0 +1,76 @@
1
+
2
+ /**
3
+ The document constructor is responsible for mapping input data from the parser
4
+ in to model.Document() objects which the rest of the pipeline expect to consume.
5
+ **/
6
+
7
+ const through = require('through2');
8
+ const Document = require('pelias-model').Document;
9
+ const peliasLogger = require( 'pelias-logger' ).get( 'openstreetmap' );
10
+ const _ = require('lodash');
11
+
12
+ module.exports = function(){
13
+
14
+ var stream = through.obj( function( item, enc, next ) {
15
+
16
+ try {
17
+ if (!item.type || ! item.id) {
18
+ throw new Error('doc without valid id or type');
19
+ }
20
+ var uniqueId = [ item.type, item.id ].join('/');
21
+
22
+ // we need to assume it will be a venue and later if it turns out to be an address it will get changed
23
+ var doc = new Document( 'openstreetmap', 'venue', uniqueId );
24
+
25
+ // Set latitude / longitude
26
+ if( item.hasOwnProperty('lat') && item.hasOwnProperty('lon') ){
27
+ doc.setCentroid({
28
+ lat: item.lat,
29
+ lon: item.lon
30
+ });
31
+ }
32
+
33
+ // Set latitude / longitude (for ways where the centroid has been precomputed)
34
+ else if( item.hasOwnProperty('centroid') ){
35
+ if( item.centroid.hasOwnProperty('lat') && item.centroid.hasOwnProperty('lon') ){
36
+ doc.setCentroid({
37
+ lat: item.centroid.lat,
38
+ lon: item.centroid.lon
39
+ });
40
+ }
41
+ }
42
+
43
+ // set bounding box
44
+ if( _.isPlainObject(item.bounds) ){
45
+ doc.setBoundingBox({
46
+ upperLeft: {
47
+ lat: parseFloat(item.bounds.n),
48
+ lon: parseFloat(item.bounds.w)
49
+ },
50
+ lowerRight: {
51
+ lat: parseFloat(item.bounds.s),
52
+ lon: parseFloat(item.bounds.e)
53
+ }
54
+ });
55
+ }
56
+
57
+ // Store osm tags as a property inside _meta
58
+ doc.setMeta( 'tags', item.tags || {} );
59
+
60
+ // Push instance of Document downstream
61
+ this.push( doc );
62
+ }
63
+
64
+ catch( e ){
65
+ peliasLogger.error( 'error constructing document model', e.stack );
66
+ }
67
+
68
+ return next();
69
+
70
+ });
71
+
72
+ // catch stream errors
73
+ stream.on( 'error', peliasLogger.error.bind( peliasLogger, __filename ) );
74
+
75
+ return stream;
76
+ };
@@ -0,0 +1,36 @@
1
+ var categoryDefaults = require('../config/category_map');
2
+
3
+ var streams = {};
4
+
5
+ streams.config = {
6
+ categoryDefaults: categoryDefaults
7
+ };
8
+
9
+ streams.pbfParser = require('./multiple_pbfs').create;
10
+ streams.docConstructor = require('./document_constructor');
11
+ streams.blacklistStream = require('pelias-blacklist-stream');
12
+ streams.tagMapper = require('./tag_mapper');
13
+ streams.adminLookup = require('pelias-wof-admin-lookup').create;
14
+ streams.addressExtractor = require('./address_extractor');
15
+ streams.categoryMapper = require('./category_mapper');
16
+ streams.addendumMapper = require('./addendum_mapper');
17
+ streams.popularityMapper = require('./popularity_mapper');
18
+ streams.dbMapper = require('pelias-model').createDocumentMapperStream;
19
+ streams.elasticsearch = require('pelias-dbclient');
20
+
21
+ // default import pipeline
22
+ streams.import = function(){
23
+ streams.pbfParser()
24
+ .pipe( streams.docConstructor() )
25
+ .pipe( streams.tagMapper() )
26
+ .pipe( streams.addressExtractor() )
27
+ .pipe( streams.blacklistStream() )
28
+ .pipe( streams.categoryMapper( categoryDefaults ) )
29
+ .pipe( streams.addendumMapper() )
30
+ .pipe( streams.popularityMapper() )
31
+ .pipe( streams.adminLookup() )
32
+ .pipe( streams.dbMapper() )
33
+ .pipe( streams.elasticsearch({name: 'openstreetmap'}) );
34
+ };
35
+
36
+ module.exports = streams;
@@ -0,0 +1,25 @@
1
+ var combinedStream = require('combined-stream');
2
+ var pbf = require('./pbf');
3
+ var path = require('path');
4
+ var logger = require('pelias-logger').get('openstreetmap');
5
+
6
+ function createCombinedStream(){
7
+ var fullStream = combinedStream.create();
8
+ var defaultPath= require('pelias-config').generate().imports.openstreetmap;
9
+
10
+ defaultPath.import.forEach(function( importObject){
11
+ var conf = {
12
+ file: path.join(defaultPath.datapath, importObject.filename),
13
+ leveldb: defaultPath.leveldbpath,
14
+ importVenues: importObject.importVenues
15
+ };
16
+ fullStream.append(function(next){
17
+ logger.info('Creating read stream for: ' + conf.file);
18
+ next(pbf.parser(conf));
19
+ });
20
+ });
21
+
22
+ return fullStream;
23
+ }
24
+
25
+ module.exports.create = createCombinedStream;