geocoder-dadata
Version:
Dadata geocoder API client that supports caching and is written purely on typescript
279 lines (211 loc) • 9.31 kB
text/typescript
import fetch, { Response } from 'node-fetch'
import { Pacekeeper } from 'pace-keeper'
const ENV_API_URL : string = 'DADATA_API_URL'
const ENV_API_TOKEN : string = 'DADATA_API_TOKEN'
const ENV_API_SECRET : string = 'DADATA_API_SECRET'
const PACE_INTERVAL : number = 1000
const PACE_LIMIT : number = 5
const DEFAULT_API_URL : string = "https://cleaner.dadata.ru/api/v1/clean/address"
const CACHE : {[key: string]: GeoResult} = {}
export interface IGeocoderOptions {
api_token? : string
api_secret? : string
api_url? : string
pace_limit? : number
cached? : boolean
}
export interface IGeoPoint {
lat : number | void
lng : number | void
}
export interface IGeoResponseForwardCoding {
"source" : string,
"result" : string,
"postal_code" : string | null,
"country" : "Россия",
"country_iso_code" : "RU",
"federal_district" : "Центральный",
"region_fias_id" : "0c5b2444-70a0-4932-980c-b4dc0d3f02b5",
"region_kladr_id" : "7700000000000",
"region_iso_code" : string | null,
"region_with_type" : string | null,
"region_type" : string | null,
"region_type_full" : string | null,
"region" : string | null,
"area_fias_id" : string | null,
"area_kladr_id" : string | null,
"area_with_type" : string | null,
"area_type" : string | null,
"area_type_full" : string | null,
"area" : string | null,
"city_fias_id" : string | null,
"city_kladr_id" : string | null,
"city_with_type" : string | null,
"city_type" : string | null,
"city_type_full" : string | null,
"city" : string | null,
"city_area" : string | null,
"city_district_fias_id" : string | null,
"city_district_kladr_id" : string | null,
"city_district_with_type" : string | null,
"city_district_type" : string | null,
"city_district_type_full" : string | null,
"city_district" : string | null,
"settlement_fias_id" : string | null,
"settlement_kladr_id" : string | null,
"settlement_with_type" : string | null,
"settlement_type" : string | null,
"settlement_type_full" : string | null,
"settlement" : string | null,
"street_fias_id" : string | null,
"street_kladr_id" : string | null,
"street_with_type" : string | null,
"street_type" : string | null,
"street_type_full" : string | null,
"street" : string | null,
"house_fias_id" : string | null,
"house_kladr_id" : string | null,
"house_type" : string | null,
"house_type_full" : string | null,
"house" : string | null,
"block_type" : string | null,
"block_type_full" : string | null,
"block" : string | null,
"entrance" : string | null,
"floor" : string | null,
"flat_fias_id" : string | null,
"flat_type" : string | null,
"flat_type_full" : string | null,
"flat" : number | null,
"flat_area" : string | number | null,
"square_meter_price" : string | number | null,
"flat_price" : string | number | null,
"postal_box" : string | null,
"fias_id" : string | null,
"fias_code" : string | null,
"fias_level" : string | number | null,
"fias_actuality_state" : string | number | null,
"kladr_id" : string | number | null,
"capital_marker" : string | number | null,
"okato" : string | number | null,
"oktmo" : string | number | null,
"tax_office" : string | number | null,
"tax_office_legal" : string | number | null,
"timezone" : string | null,
"geo_lat" : number | null,
"geo_lon" : number | null,
"beltway_hit" : string | null,
"beltway_distance" : string | null,
"qc_geo" : number,
"qc_complete" : number,
"qc_house" : number,
"qc" : number,
"unparsed_parts" : any,
"metro" : [
{
"name" : string,
"line" : string,
"distance" : number,
},
{
"name" : string,
"line" : string,
"distance" : number,
},
{
"name" : string,
"line" : string,
"distance" : number,
}
]
}
export interface IGeoResult {
ok : boolean
geo : IGeoPoint
}
export class Geocoder {
private API_TOKEN : string | undefined
private API_SECRET : string | undefined
private API_URL : string
private pace : Pacekeeper
private cached : boolean
constructor({api_token, api_secret, api_url, pace_limit, cached}: IGeocoderOptions = { cached: true }) {
Object.defineProperty(this, 'API_TOKEN', {
enumerable: false,
writable: false,
value: api_token || process.env[ENV_API_TOKEN]
})
Object.defineProperty(this, 'API_SECRET', {
enumerable: false,
writable: false,
value: api_secret || process.env[ENV_API_SECRET]
})
this.API_URL = api_url || process.env[ENV_API_URL] || DEFAULT_API_URL
this.pace = new Pacekeeper({ interval: PACE_INTERVAL, pace: pace_limit || PACE_LIMIT, parse_429: true })
this.cached = cached === undefined ? true : !!cached
}
geocode(query : string | string[]): Promise<GeoResult> {
if (!query) return Promise.resolve(build_error({ status: 0, statusText: 'empty query' }))
if (typeof query === 'string') query = [ query ]
if (!Array.isArray(query)) return Promise.resolve(build_error({ status: 0, statusText: 'bad query' }))
query = query.filter(i => i && typeof i === 'string')
if (query.length < 1) return Promise.resolve(build_error({ status: 0, statusText: 'empty query' }))
let key = Array.isArray(query) ? query.sort().join(';') : query
let body = JSON.stringify(query)
let headers = {
"Content-Type" : 'application/json',
"Authorization" : `Token ${this.API_TOKEN}`,
"X-Secret" : `${this.API_SECRET}`
}
if (this.cached) {
let cached : GeoResult | undefined = CACHE[key.toLowerCase()]
if (cached) return Promise.resolve(cached)
}
return this.pace
.submit(() => fetch(this.API_URL, { headers, body, method:'POST' })).promise
.then(res => typeof res?.json === 'function' ? res.json().then(build_result(res, this.cached, key)) : build_error(res))
}
}
export default Geocoder
function build_result(res: Response, cache: boolean = false, key: string = ''): (body: any) => GeoResult {
return function(body: any): GeoResult {
let result = new GeoResult({
status: {
code : res.status,
message : res.statusText
},
results: body
})
if (cache && key && typeof key === 'string') {
CACHE[key] = result
}
return result
}
}
function build_error(res: any): GeoResult {
return new GeoResult({
status : {
code : res?.status,
message : res?.statusText
}
})
}
class GeoResult implements IGeoResult {
public status : undefined | { code: number, message: string }
public results: undefined | IGeoResponseForwardCoding[]
constructor(data: any) {
Object.assign(this, data)
}
get total_results() : number {
return Array.isArray(this.results) ? this.results.length : 0
}
get ok() : boolean {
return this.status?.code === 200 && (this.total_results ? this.total_results > 0 : false)
}
get geo() : IGeoPoint {
return this.ok && Array.isArray(this.results) && this.results.length > 0 ? { lat: this.results[0].geo_lat || void 0, lng: this.results[0].geo_lon || void 0 } : { lat: void 0, lng: void 0 }
}
get address() : string {
return this.ok && Array.isArray(this.results) && this.results.length > 0 ? this.results[0].result : ''
}
}