var Router = require('express').Router; var elasticsearch = require('elasticsearch'); const all = require('predicates').all; const any = require('predicates').any; const not = require('predicates').not; const _ = require('lodash'); /** ----------------------- sanitizers ----------------------- **/ var sanitizers = { autocomplete: require('../sanitizer/autocomplete'), place: require('../sanitizer/place'), search: require('../sanitizer/search'), defer_to_addressit: require('../sanitizer/defer_to_addressit'), structured_geocoding: require('../sanitizer/structured_geocoding'), reverse: require('../sanitizer/reverse'), nearby: require('../sanitizer/nearby') }; /** ----------------------- middleware ------------------------ **/ var middleware = { calcSize: require('../middleware/sizeCalculator'), requestLanguage: require('../middleware/requestLanguage') }; /** ----------------------- controllers ----------------------- **/ var controllers = { coarse_reverse: require('../controller/coarse_reverse'), mdToHTML: require('../controller/markdownToHtml'), libpostal: require('../controller/libpostal'), structured_libpostal: require('../controller/structured_libpostal'), place: require('../controller/place'), placeholder: require('../controller/placeholder'), search: require('../controller/search'), search_with_ids: require('../controller/search_with_ids'), status: require('../controller/status') }; var queries = { cascading_fallback: require('../query/search'), very_old_prod: require('../query/search_original'), structured_geocoding: require('../query/structured_geocoding'), reverse: require('../query/reverse'), autocomplete: require('../query/autocomplete'), address_using_ids: require('../query/address_search_using_ids') }; /** ----------------------- controllers ----------------------- **/ var postProc = { trimByGranularity: require('../middleware/trimByGranularity'), trimByGranularityStructured: require('../middleware/trimByGranularityStructured'), distances: require('../middleware/distance'), confidenceScores: require('../middleware/confidenceScore'), confidenceScoresFallback: require('../middleware/confidenceScoreFallback'), 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'), sendJSON: require('../middleware/sendJSON'), parseBoundingBox: require('../middleware/parseBBox'), normalizeParentIds: require('../middleware/normalizeParentIds'), assignLabels: require('../middleware/assignLabels'), changeLanguage: require('../middleware/changeLanguage'), sortResponseData: require('../middleware/sortResponseData') }; // predicates that drive whether controller/search runs const hasResponseData = require('../controller/predicates/has_response_data'); const hasRequestErrors = require('../controller/predicates/has_request_errors'); const isCoarseReverse = require('../controller/predicates/is_coarse_reverse'); const isAdminOnlyAnalysis = require('../controller/predicates/is_admin_only_analysis'); const hasResultsAtLayers = require('../controller/predicates/has_results_at_layers'); const isAddressItParse = require('../controller/predicates/is_addressit_parse'); const hasRequestCategories = require('../controller/predicates/has_request_parameter')('categories'); const isOnlyNonAdminLayers = require('../controller/predicates/is_only_non_admin_layers'); // this can probably be more generalized const isRequestSourcesOnlyWhosOnFirst = require('../controller/predicates/is_request_sources_only_whosonfirst'); const hasRequestParameter = require('../controller/predicates/has_request_parameter'); const hasParsedTextProperties = require('../controller/predicates/has_parsed_text_properties'); // shorthand for standard early-exit conditions const hasResponseDataOrRequestErrors = any(hasResponseData, hasRequestErrors); const hasAdminOnlyResults = not(hasResultsAtLayers(['venue', 'address', 'street'])); const hasNumberButNotStreet = all( hasParsedTextProperties.any('number'), not(hasParsedTextProperties.any('street')) ); const serviceWrapper = require('pelias-microservice-wrapper').service; const PlaceHolder = require('../service/configurations/PlaceHolder'); const PointInPolygon = require('../service/configurations/PointInPolygon'); const Language = require('../service/configurations/Language'); const Interpolation = require('../service/configurations/Interpolation'); const Libpostal = require('../service/configurations/Libpostal'); /** * Append routes to app * * @param {object} app * @param {object} peliasConfig */ function addRoutes(app, peliasConfig) { const esclient = elasticsearch.Client(peliasConfig.esclient); const pipConfiguration = new PointInPolygon(_.defaultTo(peliasConfig.api.services.pip, {})); const pipService = serviceWrapper(pipConfiguration); const isPipServiceEnabled = _.constant(pipConfiguration.isEnabled()); const placeholderConfiguration = new PlaceHolder(_.defaultTo(peliasConfig.api.services.placeholder, {})); const placeholderService = serviceWrapper(placeholderConfiguration); const isPlaceholderServiceEnabled = _.constant(placeholderConfiguration.isEnabled()); const changeLanguageConfiguration = new Language(_.defaultTo(peliasConfig.api.services.placeholder, {})); const changeLanguageService = serviceWrapper(changeLanguageConfiguration); const isChangeLanguageEnabled = _.constant(changeLanguageConfiguration.isEnabled()); const interpolationConfiguration = new Interpolation(_.defaultTo(peliasConfig.api.services.interpolation, {})); const interpolationService = serviceWrapper(interpolationConfiguration); const isInterpolationEnabled = _.constant(interpolationConfiguration.isEnabled()); // standard libpostal should use req.clean.text for the `address` parameter const libpostalConfiguration = new Libpostal( _.defaultTo(peliasConfig.api.services.libpostal, {}), _.property('clean.text')); const libpostalService = serviceWrapper(libpostalConfiguration); // structured libpostal should use req.clean.parsed_text.address for the `address` parameter const structuredLibpostalConfiguration = new Libpostal( _.defaultTo(peliasConfig.api.services.libpostal, {}), _.property('clean.parsed_text.address')); const structuredLibpostalService = serviceWrapper(structuredLibpostalConfiguration); // fallback to coarse reverse when regular reverse didn't return anything const coarseReverseShouldExecute = all( isPipServiceEnabled, not(hasRequestErrors), not(hasResponseData), not(isOnlyNonAdminLayers) ); const libpostalShouldExecute = all( not(hasRequestErrors), not(isRequestSourcesOnlyWhosOnFirst) ); // for libpostal to execute for structured requests, req.clean.parsed_text.address must exist const structuredLibpostalShouldExecute = all( not(hasRequestErrors), hasParsedTextProperties.all('address') ); // execute placeholder if libpostal only parsed as admin-only and needs to // be geodisambiguated const placeholderGeodisambiguationShouldExecute = all( not(hasResponseDataOrRequestErrors), isPlaceholderServiceEnabled, // check request.clean for several conditions first not( any( // layers only contains venue, address, or street isOnlyNonAdminLayers, // don't geodisambiguate if categories were requested hasRequestCategories ) ), any( // only geodisambiguate if libpostal returned only admin areas or libpostal was skipped isAdminOnlyAnalysis, isRequestSourcesOnlyWhosOnFirst ) ); // execute placeholder if libpostal identified address parts but ids need to // be looked up for admin parts const placeholderIdsLookupShouldExecute = all( not(hasResponseDataOrRequestErrors), isPlaceholderServiceEnabled, // check clean.parsed_text for several conditions that must all be true all( // run placeholder if clean.parsed_text has 'street' hasParsedTextProperties.any('street'), // don't run placeholder if there's a query or category not(hasParsedTextProperties.any('query', 'category')), // run placeholder if there are any adminareas identified hasParsedTextProperties.any('neighbourhood', 'borough', 'city', 'county', 'state', 'country') ) ); const searchWithIdsShouldExecute = all( not(hasRequestErrors), // don't search-with-ids if there's a query or category not(hasParsedTextProperties.any('query', 'category')), // there must be a street hasParsedTextProperties.any('street') ); // placeholder should have executed, useful for determining whether to actually // fallback or not (don't fallback to old search if the placeholder response // should be honored as is) const placeholderShouldHaveExecuted = any( placeholderGeodisambiguationShouldExecute, placeholderIdsLookupShouldExecute ); // don't execute the cascading fallback query IF placeholder should have executed // that way, if placeholder didn't return anything, don't try to find more things the old way const fallbackQueryShouldExecute = all( not(hasRequestErrors), not(hasResponseData), not(placeholderShouldHaveExecuted) ); // defer to addressit for analysis IF there's no response AND placeholder should not have executed const shouldDeferToAddressIt = all( not(hasRequestErrors), not(hasResponseData), not(placeholderShouldHaveExecuted) ); // call very old prod query if addressit was the parser const oldProdQueryShouldExecute = all( not(hasRequestErrors), isAddressItParse ); // get language adjustments if: // - there's a response // - theres's a lang parameter in req.clean const changeLanguageShouldExecute = all( hasResponseData, not(hasRequestErrors), isChangeLanguageEnabled, hasRequestParameter('lang') ); // interpolate if: // - there's a number and street // - there are street-layer results (these are results that need to be interpolated) const interpolationShouldExecute = all( not(hasRequestErrors), isInterpolationEnabled, hasParsedTextProperties.all('number', 'street'), hasResultsAtLayers('street') ); // execute under the following conditions: // - there are no errors or data // - request is not coarse OR pip service is disabled const nonCoarseReverseShouldExecute = all( not(hasResponseDataOrRequestErrors), any( not(isCoarseReverse), not(isPipServiceEnabled) ) ); // helpers to replace vague booleans const geometricFiltersApply = true; const geometricFiltersDontApply = false; var base = '/v1/'; /** ------------------------- routers ------------------------- **/ var routers = { index: createRouter([ controllers.mdToHTML(peliasConfig.api, './public/apiDoc.md') ]), attribution: createRouter([ controllers.mdToHTML(peliasConfig.api, './public/attribution.md') ]), search: createRouter([ sanitizers.search.middleware(peliasConfig.api), middleware.requestLanguage, middleware.calcSize(), controllers.libpostal(libpostalService, libpostalShouldExecute), controllers.placeholder(placeholderService, geometricFiltersApply, placeholderGeodisambiguationShouldExecute), controllers.placeholder(placeholderService, geometricFiltersDontApply, placeholderIdsLookupShouldExecute), controllers.search_with_ids(peliasConfig.api, esclient, queries.address_using_ids, searchWithIdsShouldExecute), // 3rd parameter is which query module to use, use fallback first, then // use original search strategy if first query didn't return anything controllers.search(peliasConfig.api, esclient, queries.cascading_fallback, fallbackQueryShouldExecute), sanitizers.defer_to_addressit(shouldDeferToAddressIt), controllers.search(peliasConfig.api, esclient, queries.very_old_prod, oldProdQueryShouldExecute), postProc.trimByGranularity(), postProc.distances('focus.point.'), postProc.confidenceScores(peliasConfig.api), postProc.confidenceScoresFallback(), postProc.interpolate(interpolationService, interpolationShouldExecute), postProc.sortResponseData(require('pelias-sorting'), hasAdminOnlyResults), postProc.dedupe(), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), postProc.parseBoundingBox(), postProc.normalizeParentIds(), postProc.changeLanguage(changeLanguageService, changeLanguageShouldExecute), postProc.assignLabels(), postProc.geocodeJSON(peliasConfig.api, base), postProc.sendJSON ]), structured: createRouter([ sanitizers.structured_geocoding.middleware(peliasConfig.api), middleware.requestLanguage, middleware.calcSize(), controllers.structured_libpostal(structuredLibpostalService, structuredLibpostalShouldExecute), controllers.search(peliasConfig.api, esclient, queries.structured_geocoding, not(hasResponseDataOrRequestErrors)), postProc.trimByGranularityStructured(), postProc.distances('focus.point.'), postProc.confidenceScores(peliasConfig.api), postProc.confidenceScoresFallback(), postProc.interpolate(interpolationService, interpolationShouldExecute), postProc.dedupe(), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), postProc.parseBoundingBox(), postProc.normalizeParentIds(), postProc.changeLanguage(changeLanguageService, changeLanguageShouldExecute), postProc.assignLabels(), postProc.geocodeJSON(peliasConfig.api, base), postProc.sendJSON ]), autocomplete: createRouter([ sanitizers.autocomplete.middleware(peliasConfig.api), middleware.requestLanguage, controllers.search(peliasConfig.api, esclient, queries.autocomplete, not(hasResponseDataOrRequestErrors)), postProc.distances('focus.point.'), postProc.confidenceScores(peliasConfig.api), postProc.dedupe(), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), postProc.parseBoundingBox(), postProc.normalizeParentIds(), postProc.changeLanguage(changeLanguageService, changeLanguageShouldExecute), postProc.assignLabels(), postProc.geocodeJSON(peliasConfig.api, base), postProc.sendJSON ]), reverse: createRouter([ sanitizers.reverse.middleware, middleware.requestLanguage, middleware.calcSize(2), controllers.search(peliasConfig.api, esclient, queries.reverse, nonCoarseReverseShouldExecute), controllers.coarse_reverse(pipService, coarseReverseShouldExecute), postProc.distances('point.'), // reverse confidence scoring depends on distance from origin // so it must be calculated first postProc.confidenceScoresReverse(), postProc.dedupe(), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), postProc.parseBoundingBox(), postProc.normalizeParentIds(), postProc.changeLanguage(changeLanguageService, changeLanguageShouldExecute), postProc.assignLabels(), postProc.geocodeJSON(peliasConfig.api, base), postProc.sendJSON ]), nearby: createRouter([ sanitizers.nearby.middleware, middleware.requestLanguage, middleware.calcSize(), controllers.search(peliasConfig.api, esclient, queries.reverse, not(hasResponseDataOrRequestErrors)), postProc.distances('point.'), // reverse confidence scoring depends on distance from origin // so it must be calculated first postProc.confidenceScoresReverse(), postProc.dedupe(), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), postProc.parseBoundingBox(), postProc.normalizeParentIds(), postProc.changeLanguage(changeLanguageService, changeLanguageShouldExecute), postProc.assignLabels(), postProc.geocodeJSON(peliasConfig.api, base), postProc.sendJSON ]), place: createRouter([ sanitizers.place.middleware, middleware.requestLanguage, controllers.place(peliasConfig.api, esclient), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), postProc.parseBoundingBox(), postProc.normalizeParentIds(), postProc.changeLanguage(changeLanguageService, changeLanguageShouldExecute), postProc.assignLabels(), postProc.geocodeJSON(peliasConfig.api, base), postProc.sendJSON ]), status: createRouter([ controllers.status ]) }; //Models /** * @swagger * definitions: * standardPeliasReturn: * properties: * geocoding: * type: object * $ref: '#/definitions/geocodingObject' * type: * type: string * features: * type: array * items: * $ref: '#/definitions/featureObject' * bbox: * type: array * items: number * standardPeliasErrorReturn: * properties: * geocoding: * type: object * $ref: '#/definitions/geocodingErrorObject' * type: * type: string * features: * type: array * items: * $ref: '#/definitions/featureObject' * bbox: * type: array * items: number * geocodingObject: * properties: * version: * type: string * attribution: * type: string * query: * type: object * engine: * type: object * timestamp: * type: string * geocodingErrorObject: * properties: * version: * type: string * attribution: * type: string * query: * type: object * errors: * type: array * items: string * timestamp: * type: string * featureObject: * properties: * type: * type: string * geometry: * type: object * properties: * type: object * bbox: * type: array * items: number * convertReturn: * properties: * type: * type: string * geometry: * type: object * properties: * type: object * $ref: '#/definitions/convertPropertiesObject' * bbox: * type: array * items: number * convertPropertiesObject: * properties: * from: * type: string * to: * type: string * name: * type: string * convertErrorReturn: * properties: * errors: * type: string */ /** * @swagger * /v1: * get: * tags: * - v1 * operationId: v1 * produces: * - application/json * summary: Landing page * responses: * 200: * description: 200 ok * examples: * application/json: { "markdown": "# Pelias API\n### Version: [1.0](https://github.com/venicegeo/pelias-api/releases)\n### * [View our documentation on GitHub](https://github.com/venicegeo/pelias-documentation/blob/master/README.md)\n", "html": "