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/.dockerignore +2 -0
- package/.editorconfig +9 -0
- package/.github/workflows/push.yml +55 -0
- package/.jshintignore +6 -0
- package/.jshintrc +22 -0
- package/Dockerfile +23 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/download +3 -0
- package/bin/start +3 -0
- package/bin/test +7 -0
- package/config/category_map.js +360 -0
- package/config/features.js +52 -0
- package/config/localized_name_keys.js +44 -0
- package/config/popularity_map.js +163 -0
- package/index.js +12 -0
- package/package.json +77 -0
- package/schema/address_karlsruhe.js +25 -0
- package/schema/address_naptan.js +17 -0
- package/schema/address_osm.js +17 -0
- package/schema/address_tiger.js +18 -0
- package/schema/name_osm.js +51 -0
- package/schema.js +27 -0
- package/stream/addendum_mapper.js +71 -0
- package/stream/address_extractor.js +131 -0
- package/stream/category_mapper.js +77 -0
- package/stream/document_constructor.js +76 -0
- package/stream/importPipeline.js +36 -0
- package/stream/multiple_pbfs.js +25 -0
- package/stream/pbf.js +75 -0
- package/stream/popularity_mapper.js +80 -0
- package/stream/stats.js +47 -0
- package/stream/stringify.js +9 -0
- package/stream/tag_mapper.js +168 -0
- package/util/download_data.js +58 -0
- package/util/sendPassthroughStream.js +8 -0
package/stream/pbf.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
The pbf parser is responsible for configuring and executing a pbf parser and
|
|
4
|
+
returning a valid readable stream.
|
|
5
|
+
**/
|
|
6
|
+
|
|
7
|
+
var fs = require('fs'),
|
|
8
|
+
pbf2json = require('pbf2json'),
|
|
9
|
+
settings = require('pelias-config').generate(require('../schema')),
|
|
10
|
+
features = require('../config/features'),
|
|
11
|
+
path = require('path');
|
|
12
|
+
|
|
13
|
+
// Create a new pbf parser stream
|
|
14
|
+
function createPbfStream(opts){
|
|
15
|
+
|
|
16
|
+
var conf = config(opts);
|
|
17
|
+
|
|
18
|
+
validatePath( conf.file, 'failed to stat pbf file: ' + conf.file );
|
|
19
|
+
validatePath( conf.leveldb, 'failed to stat leveldb path: ' + conf.leveldb );
|
|
20
|
+
validateTags( conf.tags );
|
|
21
|
+
|
|
22
|
+
return pbf2json.createReadStream(conf);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Generate configuration options for pbf2json, apply default
|
|
26
|
+
// configurations where not explicitly specified.
|
|
27
|
+
function config(opts){
|
|
28
|
+
if (!opts){
|
|
29
|
+
opts = {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Use datapath setting from your config file
|
|
33
|
+
// @see: github://pelias/config for more info.
|
|
34
|
+
if(!opts.file){
|
|
35
|
+
var filename = settings.imports.openstreetmap.import[0].filename;
|
|
36
|
+
opts.file = path.join(settings.imports.openstreetmap.datapath, filename);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Use leveldb setting from your config file
|
|
40
|
+
// @see: github://pelias/config for more info.
|
|
41
|
+
if(!opts.leveldb){
|
|
42
|
+
opts.leveldb = settings.imports.openstreetmap.leveldbpath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Use default parser tags
|
|
46
|
+
if(!opts.tags){
|
|
47
|
+
// check if we import venues
|
|
48
|
+
opts.importVenues = settings.imports.openstreetmap.import[0].importVenues;
|
|
49
|
+
if(opts.importVenues){
|
|
50
|
+
opts.tags = features.tags.concat(features.venue_tags);
|
|
51
|
+
} else {
|
|
52
|
+
opts.tags = features.tags;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return opts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check path exists
|
|
59
|
+
function validatePath( path, message ){
|
|
60
|
+
try {
|
|
61
|
+
fs.statSync( path );
|
|
62
|
+
} catch( e ){
|
|
63
|
+
throw new Error( message );
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate the tag list
|
|
68
|
+
function validateTags( tags ){
|
|
69
|
+
if( !Array.isArray(tags) || !tags.length ) {
|
|
70
|
+
throw new Error( 'invalid tags' );
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports.config = config;
|
|
75
|
+
module.exports.parser = createPbfStream;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
The popularity mapper is responsible for generating a 'popularity'
|
|
3
|
+
value by inspecting OSM tags.
|
|
4
|
+
|
|
5
|
+
Disused and abandoned places are given a strong negative score.
|
|
6
|
+
If the popularity score is less than zero then the document is discarded.
|
|
7
|
+
|
|
8
|
+
Feel free to make changes to this mapping file!
|
|
9
|
+
**/
|
|
10
|
+
|
|
11
|
+
const _ = require('lodash');
|
|
12
|
+
const through = require('through2');
|
|
13
|
+
const peliasLogger = require('pelias-logger').get('openstreetmap');
|
|
14
|
+
const peliasConfig = require('pelias-config').generate();
|
|
15
|
+
const mapping = require('../config/popularity_map');
|
|
16
|
+
|
|
17
|
+
module.exports = function(){
|
|
18
|
+
|
|
19
|
+
return through.obj(( doc, enc, next ) => {
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
|
|
23
|
+
// only map venues
|
|
24
|
+
if( !['venue', 'address'].includes(doc.getLayer()) ){
|
|
25
|
+
return next(null, doc);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// skip records with no tags
|
|
29
|
+
let tags = doc.getMeta('tags');
|
|
30
|
+
if( !tags ){
|
|
31
|
+
return next( null, doc );
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// default popularity
|
|
35
|
+
let popularity = doc.getPopularity() || 0;
|
|
36
|
+
|
|
37
|
+
// apply scores from config
|
|
38
|
+
_.each(mapping, (osmTagScores, osmTagKey) => {
|
|
39
|
+
if( tags.hasOwnProperty( osmTagKey ) ){
|
|
40
|
+
// global score for the tag
|
|
41
|
+
if( osmTagScores._score ){
|
|
42
|
+
popularity += osmTagScores._score;
|
|
43
|
+
}
|
|
44
|
+
// individual scores for specific values
|
|
45
|
+
_.each(osmTagScores, (osmSubTagScores, osmTagValue) => {
|
|
46
|
+
if( osmTagValue === '_score' ){ return; }
|
|
47
|
+
if( !osmSubTagScores._score ){ return; }
|
|
48
|
+
if (_.get(tags, osmTagKey, '').trim().toLowerCase() === osmTagValue ){
|
|
49
|
+
popularity += osmSubTagScores._score;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// addresses with a popularity score GTE 10000 receieve
|
|
56
|
+
// a popularity of 1000, all others get a popularity of 0.
|
|
57
|
+
if ( doc.getLayer() === 'address' ){
|
|
58
|
+
popularity = (popularity >= 10000) ? 1000 : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// set document popularity if it is greater than zero
|
|
62
|
+
if( popularity > 0 ){ doc.setPopularity( popularity ); }
|
|
63
|
+
|
|
64
|
+
// discard places with a negative popularity
|
|
65
|
+
else if( popularity < 0 && peliasConfig.get('imports.openstreetmap.removeDisusedVenues') === true ){
|
|
66
|
+
peliasLogger.warn(`removing record ${doc.getGid()} (${doc.getName('default')}) with popularity ${popularity}`);
|
|
67
|
+
return next();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
catch( e ){
|
|
72
|
+
peliasLogger.error( 'popularity_mapper error' );
|
|
73
|
+
peliasLogger.error( e.stack );
|
|
74
|
+
peliasLogger.error( JSON.stringify( doc, null, 2 ) );
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return next( null, doc );
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
};
|
package/stream/stats.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
The stats module is useful for recording stats while developing new streams.
|
|
4
|
+
|
|
5
|
+
@usage:
|
|
6
|
+
|
|
7
|
+
tap.pipe( stream1 )
|
|
8
|
+
.pipe( stats.proxy('stream1 -> stream2') )
|
|
9
|
+
.pipe( stream2 )
|
|
10
|
+
.pipe( stats.proxy('stream2 -> stream3') )
|
|
11
|
+
.pipe( stream3 )
|
|
12
|
+
.pipe( stats.proxy('stream3 -> sink') )
|
|
13
|
+
.pipe( sink )
|
|
14
|
+
|
|
15
|
+
you can then get throughput statistics in your terminal to identify bottlenecks:
|
|
16
|
+
|
|
17
|
+
var peliasLogger = require( 'pelias-logger' ).get( 'openstreetmap' );
|
|
18
|
+
setInterval( peliasLogger.log.bind( peliasLogger, stats.metrics ), 1000 );
|
|
19
|
+
**/
|
|
20
|
+
|
|
21
|
+
var through = require('through2');
|
|
22
|
+
|
|
23
|
+
function Stats(){
|
|
24
|
+
this.reset();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Stats.prototype.reset = function(){
|
|
28
|
+
this.metrics = {};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
Stats.prototype.proxy = function( title ){
|
|
32
|
+
if( !this.metrics.hasOwnProperty( title ) ){
|
|
33
|
+
this.metrics[ title ] = 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var self = this;
|
|
37
|
+
|
|
38
|
+
var proxy = through.obj( function( item, enc, next ) {
|
|
39
|
+
self.metrics[ title ]++;
|
|
40
|
+
this.push( item );
|
|
41
|
+
return next();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return proxy;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = new Stats();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
var through = require('through2');
|
|
2
|
+
|
|
3
|
+
// convenience function for converting object streams back to strings
|
|
4
|
+
var stringify = through.obj( function( data, enc, next ){
|
|
5
|
+
this.push( JSON.stringify( data, null, 2 ), 'utf-8' );
|
|
6
|
+
next();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
module.exports = stringify;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
The tag mapper is responsible for mapping OSM tag information in to the
|
|
4
|
+
document model, using a variety of different schemas found in /schema/*.
|
|
5
|
+
**/
|
|
6
|
+
|
|
7
|
+
const _ = require('lodash');
|
|
8
|
+
const through = require('through2');
|
|
9
|
+
const peliasLogger = require('pelias-logger').get('openstreetmap');
|
|
10
|
+
|
|
11
|
+
var LOCALIZED_NAME_KEYS = require('../config/localized_name_keys');
|
|
12
|
+
var NAME_SCHEMA = require('../schema/name_osm');
|
|
13
|
+
var ADDRESS_SCHEMA = _.merge( {},
|
|
14
|
+
require('../schema/address_tiger'),
|
|
15
|
+
require('../schema/address_osm'),
|
|
16
|
+
require('../schema/address_naptan'),
|
|
17
|
+
require('../schema/address_karlsruhe')
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
module.exports = function(){
|
|
21
|
+
|
|
22
|
+
var stream = through.obj( function( doc, enc, next ) {
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
|
|
26
|
+
// skip records with no tags
|
|
27
|
+
var tags = doc.getMeta('tags');
|
|
28
|
+
if( !tags ){
|
|
29
|
+
return next( null, doc );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Unfortunately we need to iterate over every tag,
|
|
33
|
+
// so we only do the iteration once to save CPU.
|
|
34
|
+
_.each(tags, (value, key) => {
|
|
35
|
+
|
|
36
|
+
// Map localized names which begin with 'name:'
|
|
37
|
+
// @ref: http://wiki.openstreetmap.org/wiki/Namespace#Language_code_suffix
|
|
38
|
+
var suffix = getNameSuffix( key );
|
|
39
|
+
if( suffix ){
|
|
40
|
+
var val1 = trim( value );
|
|
41
|
+
if( val1 ){
|
|
42
|
+
doc.setName( suffix, val1 );
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Map name data from our name mapping schema
|
|
47
|
+
else if( _.has(NAME_SCHEMA, key) ){
|
|
48
|
+
var val2 = trim( value );
|
|
49
|
+
if( val2 ){
|
|
50
|
+
if( key === NAME_SCHEMA._primary ){
|
|
51
|
+
doc.setName( NAME_SCHEMA[key], val2 );
|
|
52
|
+
} else if ( 'default' === NAME_SCHEMA[key] ) {
|
|
53
|
+
doc.setNameAlias( NAME_SCHEMA[key], val2 );
|
|
54
|
+
} else {
|
|
55
|
+
doc.setName( NAME_SCHEMA[key], val2 );
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Map address data from our address mapping schema
|
|
61
|
+
else if( _.has(ADDRESS_SCHEMA, key) ){
|
|
62
|
+
var val3 = trim( value );
|
|
63
|
+
if( val3 ){
|
|
64
|
+
let label = ADDRESS_SCHEMA[key];
|
|
65
|
+
doc.setAddress(label, normalizeAddressField(label, val3));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Handle the case where no default name was set but there were
|
|
71
|
+
// other names which we could use as the default.
|
|
72
|
+
if( !doc.getName('default') ){
|
|
73
|
+
|
|
74
|
+
var defaultName =
|
|
75
|
+
_.get(tags, 'official_name') ||
|
|
76
|
+
_.get(tags, 'int_name') ||
|
|
77
|
+
_.get(tags, 'nat_name') ||
|
|
78
|
+
_.get(tags, 'reg_name') ||
|
|
79
|
+
doc.getName('en');
|
|
80
|
+
|
|
81
|
+
// use one of the preferred name tags listed above
|
|
82
|
+
if ( defaultName ){
|
|
83
|
+
doc.setName('default', defaultName);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// else try to use an available two-letter language name tag
|
|
87
|
+
else {
|
|
88
|
+
var keys = Object.keys(doc.name).filter(n => n.length === 2);
|
|
89
|
+
|
|
90
|
+
// unambiguous (there is only a single two-letter name tag)
|
|
91
|
+
if ( keys.length === 1 ){
|
|
92
|
+
doc.setName('default', doc.getName(keys[0]));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// note we do not handle ambiguous cases where the record contains >1
|
|
96
|
+
// two-letter name tags.
|
|
97
|
+
// see: https://github.com/pelias/openstreetmap/pull/498
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Import airport codes as aliases
|
|
102
|
+
if( tags.hasOwnProperty('aerodrome') || tags.hasOwnProperty('aeroway') ){
|
|
103
|
+
if( tags.hasOwnProperty('iata') ){
|
|
104
|
+
var iata = trim( tags.iata );
|
|
105
|
+
if( iata ){
|
|
106
|
+
doc.setNameAlias( 'default', iata );
|
|
107
|
+
doc.setNameAlias( 'default', `${iata} Airport` );
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
catch( e ){
|
|
114
|
+
peliasLogger.error( 'tag_mapper error' );
|
|
115
|
+
peliasLogger.error( e.stack );
|
|
116
|
+
peliasLogger.error( JSON.stringify( doc, null, 2 ) );
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return next( null, doc );
|
|
120
|
+
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// catch stream errors
|
|
124
|
+
stream.on( 'error', peliasLogger.error.bind( peliasLogger, __filename ) );
|
|
125
|
+
|
|
126
|
+
return stream;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Clean string of leading/trailing junk chars
|
|
130
|
+
function trim( str ){
|
|
131
|
+
return _.trim( str, '#$%^*<>-=_{};:",./?\t\n\' ' );
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// extract name suffix, eg for 'name:EN' return 'en'
|
|
135
|
+
// if not valid, return false.
|
|
136
|
+
function getNameSuffix( tag ){
|
|
137
|
+
|
|
138
|
+
if( tag.length < 6 || tag.substr(0,5) !== 'name:' ){
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// normalize suffix
|
|
143
|
+
var suffix = tag.substr(5).toLowerCase();
|
|
144
|
+
|
|
145
|
+
// check the suffix is in the localized key list
|
|
146
|
+
if( LOCALIZED_NAME_KEYS.indexOf(suffix) === -1 ){
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return suffix;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// apply some very basic normalization
|
|
154
|
+
// note: in the future we should consider doing something more advanced like:
|
|
155
|
+
// https://github.com/pelias/openaddresses/pull/477
|
|
156
|
+
function normalizeAddressField(key, value) {
|
|
157
|
+
if (key === 'street') {
|
|
158
|
+
|
|
159
|
+
// contract English diagonals
|
|
160
|
+
value = value
|
|
161
|
+
.replace(/\b(northwest)\b/i, 'NW')
|
|
162
|
+
.replace(/\b(northeast)\b/i, 'NE')
|
|
163
|
+
.replace(/\b(southwest)\b/i, 'SW')
|
|
164
|
+
.replace(/\b(southeast)\b/i, 'SE');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const async = require('async');
|
|
5
|
+
const child_process = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const config = require( 'pelias-config' ).generate(require('../schema'));
|
|
8
|
+
const logger = require('pelias-logger').get('openstreetmap-download');
|
|
9
|
+
|
|
10
|
+
if (require.main === module) {
|
|
11
|
+
download((err) => {
|
|
12
|
+
if (err) {
|
|
13
|
+
logger.error('Failed to download data', err);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
logger.info('All done!');
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function download(callback) {
|
|
21
|
+
let sources;
|
|
22
|
+
|
|
23
|
+
// if no download sources are specified, default to the planet file
|
|
24
|
+
if (_.isEmpty(config.imports.openstreetmap.download)) {
|
|
25
|
+
sources = [
|
|
26
|
+
'https://planet.openstreetmap.org/pbf/planet-latest.osm.pbf'
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
sources = _.map(config.imports.openstreetmap.download, (source) => source.sourceURL);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
logger.info(`Downloading sources: ${sources}`);
|
|
34
|
+
|
|
35
|
+
fs.mkdir(config.imports.openstreetmap.datapath, { recursive: true }, (err) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
logger.error(`error making directory ${config.imports.openstreetmap.datapath}`, err);
|
|
38
|
+
return callback(err);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async.forEach(
|
|
42
|
+
sources,
|
|
43
|
+
(source, next) => {
|
|
44
|
+
downloadSource(config, source, next);
|
|
45
|
+
},
|
|
46
|
+
callback
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function downloadSource(config, sourceUrl, callback) {
|
|
52
|
+
const targetDir = config.imports.openstreetmap.datapath;
|
|
53
|
+
|
|
54
|
+
logger.debug(`downloading ${sourceUrl} to ${targetDir}`);
|
|
55
|
+
child_process.exec(`cd ${targetDir} && { curl -L -X GET --silent --fail --remote-name ${sourceUrl}; }`, callback);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = download;
|