hk-address-parser-lib
Version:
The library for the HKAddressParser project
1,836 lines (1,600 loc) • 268 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var turf = require('@turf/turf');
var proj4 = _interopDefault(require('proj4'));
var Stream = _interopDefault(require('stream'));
var http = _interopDefault(require('http'));
var Url = _interopDefault(require('url'));
var https = _interopDefault(require('https'));
var zlib = _interopDefault(require('zlib'));
const region = {
HK: {
eng: "Hong Kong",
chi: "香港"
},
KLN: {
eng: "Kowloon",
chi: "九龍"
},
NT: {
eng: "New Territories",
chi: "新界"
}
};
const dcDistrict = {
invalid: {
eng: "Invalid District Name",
chi: "無效地區"
},
CW: {
eng: "Central and Western District",
chi: "中西區"
},
EST: {
eng: "Eastern District",
chi: "東區"
},
ILD: {
eng: "Islands District",
chi: "離島區"
},
KLC: {
eng: "Kowloon City District",
chi: "九龍城區"
},
KC: {
eng: "Kwai Tsing District",
chi: "葵青區"
},
KT: {
eng: "Kwun Tong District",
chi: "觀塘區"
},
NTH: {
eng: "North District",
chi: "北區"
},
SK: {
eng: "Sai Kung District",
chi: "西貢區"
},
ST: {
eng: "Sha Tin Distric",
chi: "沙田區"
},
SSP: {
eng: "Sham Shui Po District",
chi: "深水埗區"
},
STH: {
eng: "Southern District",
chi: "南區"
},
TP: {
eng: "Tai Po District",
chi: "大埔區"
},
TW: {
eng: "Tsuen Wan District",
chi: "荃灣區"
},
TM: {
eng: "Tuen Mun District",
chi: "屯門區"
},
WC: {
eng: "Wan Chai District",
chi: "灣仔區"
},
WTS: {
eng: "Wong Tai Sin District",
chi: "黃大仙區"
},
YTM: {
eng: "Yau Tsim Mong District",
chi: "油尖旺區"
},
YL: {
eng: "Yuen Long District",
chi: "元朗區"
}
};
// MUST use common js style to let address-parse "require" work normally
var constants = {
region,
dcDistrict
};
var constants_1 = constants.region;
var constants_2 = constants.dcDistrict;
/**
* To parse the address from OGCIO. this file should be compatible with node.js and js running in browser
*
*/
const CONFIDENT_ALL_MATCH = 1.0;
const CONFIDENT_MULTIPLIER_NAME_ONLY = 0.5;
const CONFIDENT_MULTIPLIER_PARTIAL_MATCH = 0.7;
const CONFIDENT_MULTIPLIER_OPPOSITE_STREET = 0.75;
const CONFIDENT_MULTIPLIER_FULL_STREET_MATCH = 1.5;
const OGCIO_KEY_BLOCK = "Block";
const OGCIO_KEY_PHASE = "Phase";
const OGCIO_KEY_ESTATE = "Estate";
const OGCIO_KEY_VILLAGE = "Village";
const OGCIO_KEY_STREET = "Street";
const OGCIO_KEY_REGION = "Region";
const OGCIO_KEY_BUILDING_NAME = "BuildingName";
const SCORE_SCHEME = {
[OGCIO_KEY_BUILDING_NAME]: 50,
[OGCIO_KEY_VILLAGE]: 40,
[OGCIO_KEY_ESTATE]: 40,
[OGCIO_KEY_STREET]: 40,
[OGCIO_KEY_REGION]: 20,
[OGCIO_KEY_PHASE]: 20,
[OGCIO_KEY_BLOCK]: 20,
};
// priority in asscending order
const elementPriority = [
OGCIO_KEY_BUILDING_NAME,
OGCIO_KEY_BLOCK,
OGCIO_KEY_PHASE,
OGCIO_KEY_ESTATE,
OGCIO_KEY_VILLAGE,
OGCIO_KEY_STREET,
OGCIO_KEY_REGION
];
const log = console.log; // eslint-disable-line
class Match {
constructor(confident, matchedKey, matchedWords) {
this.confident = confident;
this.matchedKey = matchedKey;
// array of words that matched
this.matchedWords = matchedWords;
}
}
function removeFloor(address) {
return address.replace(/([0-9A-z\-\s]+[樓層]|[0-9A-z號\-\s]+[舖鋪]|地[下庫]|平台).*/g, '');
}
function dcDistrictMapping(val, isChinese) {
for (const district in constants_2) {
if (district === val) {
return isChinese ? constants_2[district].chi : constants_2[district].eng;
}
}
return isChinese ? constants_2.invalid.chi : constants_2.invalid.eng;
}
function regionMapping(val) {
for (const reg in constants_1) {
if (reg === val) {
return constants_1[reg].eng;
}
}
}
/**
* Return the percentage of how much do the laterString match the first one
* @param {*} string
* @param {*} stringToSearch
*/
function findPartialMatch(string, stringToSearch) {
const match = {
matchPercentage: 0,
matchedWord: null
};
// some exceptional case if the word from OGCIO contains directly the search address, we consider it as a full match
if (stringToSearch.indexOf(string) >= 0) {
match.matchPercentage = 0.9;
match.matchedWord = string;
} else {
masterLoop:
for (let i = 0; i < stringToSearch.length; i ++) {
for (let end = stringToSearch.length; end > i; end --) {
const substring = stringToSearch.substring(i, end);
if (string.includes(substring)) {
match.matchPercentage = (substring.length * 1.0 / stringToSearch.length);
match.matchedWord = substring;
break masterLoop;
}
}
}
}
return match;
}
/**
* Remove the top level "Eng"/"Chi" prefix of the addresses
* @param {*} data
*/
function eliminateLangKeys(data) {
const result = {};
for (const key of Object.keys(data)) {
const refinedKey = key.replace(/(^Chi|^Eng)/,'');
// eliminate with recursion
if (typeof(data[key]) === "object") {
result[refinedKey] = eliminateLangKeys(data[key]);
} else {
result[refinedKey] = data[key];
}
}
return result;
}
function normalizeResponse(responseFromOGCIO) {
// No more flatten json to maintain the orginal data structure
// https://www.als.ogcio.gov.hk/docs/Data_Dictionary_for_ALS_EN.pdf
return responseFromOGCIO.SuggestedAddress.map(record => ({
chi: eliminateLangKeys(record.Address.PremisesAddress.ChiPremisesAddress),
eng: eliminateLangKeys(record.Address.PremisesAddress.EngPremisesAddress),
geo: record.Address.PremisesAddress.GeospatialInformation,
}));
}
function tryToMatchAnyNumber(address, number) {
const matches = address.match(/\d+/g);
if (matches === null) {
return false;
}
for (const match of matches) {
const num = parseInt(match, 10);
if (num === number) {
return true;
}
}
return false;
}
function tryToMatchRangeOfNumber(address, from, to, isOdd) {
const matches = address.match(/\d+/g);
if (matches === null) {
return false;
}
for (const match of matches) {
const num = parseInt(match, 10);
if (num >= from && num <= to && ((num % 2 === 1) === isOdd)) {
return true;
}
}
return false;
}
function isChinese(s) {
return /[^\u0000-\u00ff]/.test(s);
}
function splitValueForSpaceIfChinese(value) {
if (isChinese(value) && /\s/.test(value)) {
const tokens = value.split(/\s/);
// we need the last element only
return tokens[tokens.length - 1];
}
return value;
}
function matchAllMatchedWords(address, matchedWords) {
return matchedWords.map(word => address.includes(word)).reduce((p,c) => p && c, true);
}
/**
* Find the longest set of matches that has highest score and not overlapping
* @param {*} address
* @param {*} matches
*/
function findMaximumNonOverlappingMatches(address, matches) {
if (matches.length === 1) {
if (matches[0].matchedWord !== null && matchAllMatchedWords(address, matches[0].matchedWords)) {
return matches;
}
return [];
}
let longestMatchScore = 0;
let longestMatch = [];
for (const match of matches) {
if (matchAllMatchedWords(address, match.matchedWords)) {
let subAddress = address;
match.matchedWords.forEach(word => subAddress = subAddress.replace(word, ''));
const localLongestMatch = findMaximumNonOverlappingMatches(subAddress, matches.filter(m => m.matchedKey !== match.matchedKey));
localLongestMatch.push(match);
const score = calculateScoreFromMatches(localLongestMatch);
if (score > longestMatchScore) {
longestMatchScore = score;
longestMatch = localLongestMatch;
}
}
}
return longestMatch;
}
/**
* To calcutate the final confident with partical match
* @param {*} confident
* @param {*} matchPercentage
*/
function modifyConfidentByPartialMatchPercentage(confident, matchPercentage) {
return confident * matchPercentage * matchPercentage * CONFIDENT_MULTIPLIER_PARTIAL_MATCH;
}
function calculateScoreFromMatches(matches) {
let score = 0;
for (const match of matches) {
score += SCORE_SCHEME[match.matchedKey] * match.confident;
}
return score;
}
function searchSimilarityForStreetOrVillage(type, address, addressToSearch, BuildingNoFrom, BuildingNoTo) {
const sim = new Match(0, type, []);
if (address.includes(addressToSearch)) {
sim.confident = CONFIDENT_ALL_MATCH;
sim.matchedWords.push(addressToSearch);
} else {
const { matchPercentage, matchedWord } = findPartialMatch(address, addressToSearch);
if (matchPercentage > 0) {
sim.confident = modifyConfidentByPartialMatchPercentage(CONFIDENT_ALL_MATCH, matchPercentage);
sim.matchedWords.push(matchedWord);
}
}
// total match of the streetname
if (BuildingNoFrom) {
const from = parseInt(BuildingNoFrom, 10);
const to = BuildingNoTo ? parseInt(BuildingNoTo, 10) : from;
const isOdd = parseInt(BuildingNoFrom, 10) % 2 === 1;
// If the street name and also the street no. is matched. we should give it a very high score
if (from === to) {
if (!tryToMatchAnyNumber(address, from)) {
if (tryToMatchRangeOfNumber(address, from, to, !isOdd)) {
// ratio 1
sim.confident *= CONFIDENT_MULTIPLIER_OPPOSITE_STREET;
} else {
sim.confident *= CONFIDENT_MULTIPLIER_NAME_ONLY;
}
} else {
sim.matchedWords.push(from + '');
sim.confident *= CONFIDENT_MULTIPLIER_FULL_STREET_MATCH;
}
} else {
if (!tryToMatchRangeOfNumber(address, from, to, isOdd)) {
// Try to look up at opposite street
if (tryToMatchRangeOfNumber(address, from, to, !isOdd)) {
// ratio 1
sim.confident *= CONFIDENT_MULTIPLIER_OPPOSITE_STREET;
} else {
sim.confident *= CONFIDENT_MULTIPLIER_NAME_ONLY;
}
} else {
// TODO: cannot mark the street/village number that we have came across
sim.confident *= CONFIDENT_MULTIPLIER_FULL_STREET_MATCH;
}
}
} else {
sim.confident *= CONFIDENT_MULTIPLIER_NAME_ONLY;
}
return sim;
}
function searchOccuranceForBlock(address, { BlockDescriptor, BlockNo}) {
if (address.includes(BlockNo + BlockDescriptor)) {
const match = new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_BLOCK, [BlockNo, BlockDescriptor]);
if (BlockNo) {
if (!tryToMatchAnyNumber(address, parseInt(BlockNo, 10))) {
match.confident = CONFIDENT_MULTIPLIER_NAME_ONLY;
}
}
return match;
}
return null;
}
function searchOccuranceForPhase(address, { PhaseNo, PhaseName}) {
if (address.includes(PhaseName + PhaseNo)) {
const match = new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_PHASE, [PhaseNo, PhaseName]);
if (PhaseNo) {
if (!tryToMatchAnyNumber(address, parseInt(PhaseNo, 10))) {
match.confident = CONFIDENT_MULTIPLIER_NAME_ONLY;
}
}
return match;
}
return null;
}
function searchOccuranceForEstate(address, { EstateName }) {
if (address.includes(EstateName)) {
return new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_ESTATE, [EstateName]);
}
return null;
}
function searchOccuranceForRegion(address, region) {
if (address.includes(region)) {
return new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_REGION, [region]);
}
return null;
}
function searchOccuranceForBuildingName(address, buildingName) {
if (address.includes(buildingName)) {
return new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_BUILDING_NAME, [buildingName]);
} else {
const { matchPercentage, matchedWord } = findPartialMatch(address, buildingName);
if (matchPercentage > 0) {
const match = new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_BUILDING_NAME, [matchedWord]);
match.confident = modifyConfidentByPartialMatchPercentage(match.confident, matchPercentage);
return match;
}
}
return null;
}
function searchOccuranceForStreet(address, {StreetName, BuildingNoFrom, BuildingNoTo}) {
const streetsToTest = splitValueForSpaceIfChinese(StreetName);
return searchSimilarityForStreetOrVillage(OGCIO_KEY_STREET, address, streetsToTest, BuildingNoFrom, BuildingNoTo);
}
function searchOccuranceForVillage(address, {VillageName, BuildingNoFrom, BuildingNoTo}) {
const streetsToTest = splitValueForSpaceIfChinese(VillageName);
return searchSimilarityForStreetOrVillage(OGCIO_KEY_VILLAGE, address, streetsToTest, BuildingNoFrom, BuildingNoTo);
}
/**
* Take a
*/
function searchOccurance(address, ogcioRecordElementKey, ogcioRecordElement) {
switch (ogcioRecordElementKey) {
case OGCIO_KEY_STREET: return searchOccuranceForStreet(address, ogcioRecordElement); break;
case OGCIO_KEY_VILLAGE: return searchOccuranceForVillage(address, ogcioRecordElement); break;
case OGCIO_KEY_BLOCK: return searchOccuranceForBlock(address, ogcioRecordElement); break;
case OGCIO_KEY_PHASE: return searchOccuranceForPhase(address, ogcioRecordElement); break;
case OGCIO_KEY_ESTATE: return searchOccuranceForEstate(address, ogcioRecordElement); break;
case OGCIO_KEY_REGION: return searchOccuranceForRegion(address, ogcioRecordElement); break;
case OGCIO_KEY_BUILDING_NAME: return searchOccuranceForBuildingName(address, ogcioRecordElement); break;
}
return null;
}
function findMatchFromOGCIORecord(address, ogcioRecord) {
const matches = [];
// First we look up everything that exists in that address
for (const key of elementPriority) {
if (ogcioRecord.chi[key] !== undefined && isChinese(address)) {
//
const occurance = searchOccurance(address, key, ogcioRecord.chi[key]);
if (occurance === null) {
continue;
}
matches.push(occurance);
}
if (ogcioRecord.eng[key] !== undefined && !isChinese(address)) {
const occurance = searchOccurance(address, key, ogcioRecord.eng[key]);
if (occurance === null) {
continue;
}
matches.push(occurance);
}
}
return findMaximumNonOverlappingMatches(address, matches);
}
function transformDistrict(ogcioRecord) {
if (ogcioRecord.eng.District) {
ogcioRecord.eng.District.DcDistrict = dcDistrictMapping(ogcioRecord.eng.District.DcDistrict, false);
}
if (ogcioRecord.chi.District) {
ogcioRecord.chi.District.DcDistrict = dcDistrictMapping(ogcioRecord.chi.District.DcDistrict, true);
}
if (ogcioRecord.eng.Region) {
ogcioRecord.eng.Region = regionMapping(ogcioRecord.eng.Region);
}
return ogcioRecord;
}
function parseAddress(address, normalizedOGCIOResult) {
for (let record of normalizedOGCIOResult) {
const matches = findMatchFromOGCIORecord(address, record);
record.score = calculateScoreFromMatches(matches);
record.matches = matches;
// Also tranform the district code to name directly
record = transformDistrict(record);
}
normalizedOGCIOResult = normalizedOGCIOResult.sort((a, b) => {
return b.score - a.score;
});
return (normalizedOGCIOResult.slice(0, 200));
}
/**
* Standalone version of address parsing.
* @param {*} address
* @param {*} responseFromOGCIO Raw json response from ogcio
*/
function searchResult(address, responseFromOGCIO) {
const normalizedAddress = removeFloor(address).toUpperCase();
const normalizedOGCIOResult = normalizeResponse(responseFromOGCIO);
return parseAddress(normalizedAddress, normalizedOGCIOResult);
}
var ogcioParser = {
searchResult,
calculateScoreFromMatches
};
class Address {
constructor() {
}
/**
* Return the detailed components of the parsed address
* each element should be in form of
* {
* translatedLabel:
* key:
* translatedValue:
* }
*/
components(lang) {
return [];
}
componentLabelForKey(key, lang) {
const component = this.components(lang).find(component => component.key === key);
return component === undefined ? '' : component.translatedLabel;
}
componentValueForKey(key, lang) {
const component = this.components(lang).find(component => component.key === key);
return component === undefined ? '' : component.translatedValue;
}
fullAddress(lang) {
return null;
}
coordinate() {
return {
lat: 0,
lng: 0,
}
}
coordinates() {
return [];
}
// In the future it can be multiple source
dataSource() {
return null;
}
/**
* Return a normalized confident level from 0 - 10
*/
confidence() {
return 0;
}
distanceTo(address) {
const cord1 = turf.point([this.coordinate().lng, this.coordinate().lat]);
const cord2 = turf.point([address.coordinate().lng, address.coordinate().lat]);
return turf.distance(cord1, cord2, {units: 'kilometers'});
}
}
Address.LANG_EN = 'eng';
Address.LANG_ZH = 'chi';
/**
* Some helper functions for showing OGCIO address result
* definations:
* https://www.als.ogcio.gov.hk/docs/Data_Dictionary_for_ALS_EN.pdf
*/
const OGCIO_KEY_BLOCK$1 = 'Block';
const OGCIO_KEY_PHASE$1 = 'Phase';
const OGCIO_KEY_ESTATE$1 = 'Estate';
const OGCIO_KEY_VILLAGE$1 = 'Village';
const OGCIO_KEY_REGION$1 = 'Region';
const OGCIO_KEY_STREET$1 = 'Street';
const OGCIO_KEY_DISTRICT = 'District';
const OGCIO_KEY_BUILDING_NAME$1 = 'BuildingName';
const keys = {
eng: {
// level 1 keys
[OGCIO_KEY_BLOCK$1]: 'Block',
[OGCIO_KEY_PHASE$1]: 'Phase',
[OGCIO_KEY_ESTATE$1]: 'Estate',
[OGCIO_KEY_VILLAGE$1]: 'Village',
[OGCIO_KEY_REGION$1]: 'Region',
[OGCIO_KEY_DISTRICT]: 'District',
[OGCIO_KEY_STREET$1]: 'Street',
[OGCIO_KEY_BUILDING_NAME$1]: 'Building Name',
// level 2 keys
[`${OGCIO_KEY_DISTRICT}.DcDistrict`]: '區議會分區',
[`${OGCIO_KEY_STREET$1}.StreetName`]: 'Street Name',
[`${OGCIO_KEY_STREET$1}.BuildingNoFrom`]: 'Street No. From',
[`${OGCIO_KEY_STREET$1}.BuildingNoTo`]: 'Street No. To',
[`${OGCIO_KEY_ESTATE$1}.EstateName`]: 'Estate Name',
},
chi: {
// level 1 keys
[OGCIO_KEY_BLOCK$1]: '座數',
[OGCIO_KEY_PHASE$1]: '期數',
[OGCIO_KEY_ESTATE$1]: '屋邨',
[OGCIO_KEY_VILLAGE$1]: '鄉村',
[OGCIO_KEY_REGION$1]: '區域',
[OGCIO_KEY_DISTRICT]: '地區',
[OGCIO_KEY_STREET$1]: '街道',
[OGCIO_KEY_BUILDING_NAME$1]: '大廈名稱',
// level 2 keys
[`${OGCIO_KEY_DISTRICT}.DcDistrict`]: '區議會分區',
[`${OGCIO_KEY_STREET$1}.StreetName`]: '街道',
[`${OGCIO_KEY_STREET$1}.BuildingNoFrom`]: '街號',
[`${OGCIO_KEY_STREET$1}.BuildingNoTo`]: '街號',
[`${OGCIO_KEY_ESTATE$1}.EstateName`]: '屋邨',
}
};
function safeFieldValue(obj, key) {
return obj && obj[key] ? obj[key] : '';
}
function textForKey(key, lang) {
return keys[lang]
? (keys[lang][key]
? keys[lang][key]
: key)
: key;
}
function engBuildingNumberFromField(field) {
if (!field || (!field.BuildingNoFrom && !field.BuildingNoTo)) {
return '';
}
if (field.BuildingNoFrom && field.BuildingNoTo) {
return `${field.BuildingNoFrom}-${field.BuildingNoTo}`;
} else {
return `${field.BuildingNoTo ? field.BuildingNoTo : field.BuildingNoFrom}`;
}
}
function chineseBuildingNumberFromField(field) {
if (!field || (!field.BuildingNoFrom && !field.BuildingNoTo)) {
return '';
}
if (field.BuildingNoFrom && field.BuildingNoTo) {
return `${field.BuildingNoFrom}至${field.BuildingNoTo}號`;
} else {
return `${field.BuildingNoTo ? field.BuildingNoTo : field.BuildingNoFrom}號`;
}
}
function prettyPrintBlock(blockObj, lang) {
if (lang === 'chi') {
return `${blockObj.BlockNo}${blockObj.BlockDescriptor}`;
} else if (lang === 'eng') {
return `${blockObj.BlockDescriptor} ${blockObj.BlockNo}`;
}
}
function prettyPrintEstate(estateObj, lang) {
let estateName = estateObj.EstateName;
const phase = estateObj[OGCIO_KEY_PHASE$1];
if (lang === 'chi') {
if (phase) {
estateName = `${estateName}${safeFieldValue(estateObj[OGCIO_KEY_PHASE$1], 'PhaseNo')}${safeFieldValue(estateObj[OGCIO_KEY_PHASE$1], 'PhaseName')}`;
}
} else if (lang === 'eng') {
if (phase) {
estateName = `${safeFieldValue(estateObj[OGCIO_KEY_PHASE$1], 'PhaseName')}${safeFieldValue(estateObj[OGCIO_KEY_PHASE$1], 'PhaseNo')},${estateName}`;
}
}
return estateName;
}
function prettyPrintStreet(streetObj, lang) {
if (lang === 'chi') {
return `${safeFieldValue(streetObj, 'StreetName')}${chineseBuildingNumberFromField(streetObj)}`;
} else if (lang === 'eng') {
return `${engBuildingNumberFromField(streetObj)} ${safeFieldValue(streetObj, 'StreetName')}`;
}
}
function textForValue(record, key, lang) {
if (!record[lang]) {
return '';
}
if (typeof (record[lang][key]) === 'string') {
return record[lang][key];
}
if (key === OGCIO_KEY_ESTATE$1) {
return prettyPrintEstate(record[lang][key], lang);
} else if (key === OGCIO_KEY_BLOCK$1) {
return prettyPrintBlock(record[lang][key], lang);
} else if (key === OGCIO_KEY_STREET$1) {
return prettyPrintStreet(record[lang][key], lang);
}
return Object.values(record[lang][key]).join();
}
/**
* Format the chinese address from the given result set
* @param {*} result
*/
function fullChineseAddressFromResult(result) {
const { Street, Block, Phase, Estate, Village } = result;
const region = safeFieldValue(result, 'Region');
const streetName = safeFieldValue(Street, 'StreetName');
const streetNumber = chineseBuildingNumberFromField(Street);
const villageName = safeFieldValue(Village, 'VillageName');
const villageNumber = chineseBuildingNumberFromField(Village);
const estateName = safeFieldValue(Estate, 'EstateName');
const buildingName = safeFieldValue(result, 'BuildingName');
return `${region}${villageName}${villageNumber}${streetName}${streetNumber}${estateName}${buildingName}`.trim();
}
/**
* Format the english address from the given result set
* @param {*} result
*/
function fullEnglishAddressFromResult(result) {
const { Street, Block, Phase, Estate, Village } = result;
const region = safeFieldValue(result, 'Region');
const streetName = safeFieldValue(Street, 'StreetName');
const streetNumber = engBuildingNumberFromField(Street);
const villageName = safeFieldValue(Village, 'VillageName');
const villageNumber = engBuildingNumberFromField(Village);
const buildingName = safeFieldValue(result, 'BuildingName');
const estateName = safeFieldValue(Estate, 'EstateName');
return [buildingName, estateName, `${streetNumber} ${streetName}`, `${villageNumber} ${villageName}`, region].filter(token => token.match(/\S/)).join(', ');
}
var ogcioHelper = {
topLevelKeys: () => Object.keys(keys.chi).map((key) => ({ key, value: keys.chi[key] })).filter(key => !key.key.includes('.')),
textForKey,
textForValue,
fullEnglishAddressFromResult,
fullChineseAddressFromResult
};
const toFloat = (value) => {
if (typeof value === 'number') {
return value;
}
let val = parseFloat(value);
if (isNaN(val)) {
return 0;
}
return val;
};
class OGCIOAddress extends Address {
constructor(ogcioRecord) {
super();
this.record = ogcioRecord;
this.flattenedComponents = null;
}
components(lang) {
if (this.flattenedComponents === null) {
this.flattenedComponents = this.flattenComponents();
}
if (lang === Address.LANG_EN) {
return this.flattenedComponents['eng'];
} else {
return this.flattenedComponents['chi'];
}
}
flattenComponents() {
const flattenedComponents = {
[Address.LANG_EN]: [],
[Address.LANG_ZH]: [],
};
const langs = [Address.LANG_ZH, Address.LANG_EN];
for (const lang of langs) {
for (const key of Object.keys(this.record[lang])) {
flattenedComponents[lang].push({
key,
translatedLabel: ogcioHelper.textForKey(key, lang),
translatedValue: ogcioHelper.textForValue(this.record, key, lang),
});
}
}
return flattenedComponents;
}
fullAddress(lang) {
if (lang === Address.LANG_EN) {
return ogcioHelper.fullEnglishAddressFromResult(this.record['eng']);
} else {
return ogcioHelper.fullChineseAddressFromResult(this.record['chi']);
}
}
coordinate() {
const geo = {
lat: 0,
lng: 0,
};
if (this.record.geo !== undefined && this.record.geo.length > 0) {
geo.lat = toFloat(this.record.geo[0].Latitude);
geo.lng = toFloat(this.record.geo[0].Longitude);
}
return geo;
}
coordinates() {
if (this.record.geo !== undefined && this.record.geo.length > 0) {
return this.record.geo.map(geo => ({
lat: toFloat(geo.Latitude),
lng: toFloat(geo.Longitude)
}));
}
return [];
}
// In the future it can be multiple source
dataSource() {
return '資科辦';
}
confidence() {
return Math.min(
4,
(this.record.matches
.filter(match => match.matchedKey === key)
.map(match => match.confident)
.reduce((p, c) => c, 0) *
5) |
0
);
}
}
class LandAddress extends Address{
constructor(landRecord) {
super();
this.record = landRecord;
}
/**
* Return the detailed components of the parsed address
* each element should be in form of
* {
* translatedLabel:
* key:
* translatedValue:
* }
*/
components(lang) {
if (lang === Address.LANG_EN) {
return [{
translatedValue: this.record.nameEN,
key: 'name',
translatedLabel: "Name"
}];
} else if (lang === Address.LANG_ZH) {
return [{
translatedValue: this.record.nameZH,
key: 'name',
translatedLabel: "Name"
}];
}
}
fullAddress(lang) {
if (lang === Address.LANG_EN) {
return this.record.addressEN;
} else if (lang === Address.LANG_ZH) {
return this.record.addressZH;
}
}
coordinate() {
return {
lat: toFloat(this.record.lat),
lng: toFloat(this.record.lng),
}
}
coordinates() {
return [{
lat: toFloat(this.record.lat),
lng: toFloat(this.record.lng),
}];
}
// In the future it can be multiple source
dataSource() {
return '地政總署';
}
/**
* Return a normalized confident level from 0 - 10
*/
confidence() {
return 0;
}
}
const createAddress = function (type, record) {
switch (type) {
case "ogcio": return new OGCIOAddress(record);
case "land" : return new LandAddress(record);
default: return new Address(record);
}
};
proj4.defs([
[
"EPSG:2326",
"+proj=tmerc +lat_0=22.31213333333334 +lon_0=114.1785555555556 +k=1 +x_0=836694.05 +y_0=819069.8 +ellps=intl +towgs84=-162.619,-276.959,-161.764,0.067753,-2.24365,-1.15883,-1.09425 +units=m +no_defs"],
[
"EPSG:4326",
"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
]
]);
var ProjConvertor = {
projTransform: (fromProjection, toProjection, coordinates) => {
return proj4(proj4(fromProjection), proj4(toProjection), coordinates)
}
};
const normalizeString = (str) => {
return str.replace(/,/g, ' ')
};
const sortLandResult = (searchString, landResults) => {
const normalizeSearchString = normalizeString(searchString);
const container = landResults.map(
landAddress => ({
landAddress,
lcs: Math.max(
lcs(normalizeSearchString, normalizeString(landAddress.fullAddress('chi'))),
lcs(normalizeSearchString, normalizeString(landAddress.fullAddress('eng')))
),
})
);
container.sort( (a, b) => b.lcs - a.lcs);
return container.map(c => c.landAddress);
};
const lcs = (str1, str2) => {
const m = str1.length + 1;
const n = str2.length + 1;
const lcsTable = new Array(m);
for (let i = 0; i < m; i++) {
lcsTable[i] = new Array(n);
}
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (i === 0 || j === 0) {
lcsTable[i][j] = 0;
} else if (str1[i] === str2[j]) {
lcsTable[i][j] = 1 + lcsTable[i - 1][j - 1];
} else {
lcsTable[i][j] = Math.max(lcsTable[i - 1][j], lcsTable[i][j - 1]);
}
}
}
return lcsTable[m - 1][n - 1];
};
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function unwrapExports (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x.default : x;
}
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
function getCjsExportFromNamespace (n) {
return n && n.default || n;
}
// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js
// (MIT licensed)
const BUFFER = Symbol('buffer');
const TYPE = Symbol('type');
class Blob {
constructor() {
this[TYPE] = '';
const blobParts = arguments[0];
const options = arguments[1];
const buffers = [];
if (blobParts) {
const a = blobParts;
const length = Number(a.length);
for (let i = 0; i < length; i++) {
const element = a[i];
let buffer;
if (element instanceof Buffer) {
buffer = element;
} else if (ArrayBuffer.isView(element)) {
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
} else if (element instanceof ArrayBuffer) {
buffer = Buffer.from(element);
} else if (element instanceof Blob) {
buffer = element[BUFFER];
} else {
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
}
buffers.push(buffer);
}
}
this[BUFFER] = Buffer.concat(buffers);
let type = options && options.type !== undefined && String(options.type).toLowerCase();
if (type && !/[^\u0020-\u007E]/.test(type)) {
this[TYPE] = type;
}
}
get size() {
return this[BUFFER].length;
}
get type() {
return this[TYPE];
}
slice() {
const size = this.size;
const start = arguments[0];
const end = arguments[1];
let relativeStart, relativeEnd;
if (start === undefined) {
relativeStart = 0;
} else if (start < 0) {
relativeStart = Math.max(size + start, 0);
} else {
relativeStart = Math.min(start, size);
}
if (end === undefined) {
relativeEnd = size;
} else if (end < 0) {
relativeEnd = Math.max(size + end, 0);
} else {
relativeEnd = Math.min(end, size);
}
const span = Math.max(relativeEnd - relativeStart, 0);
const buffer = this[BUFFER];
const slicedBuffer = buffer.slice(relativeStart, relativeStart + span);
const blob = new Blob([], { type: arguments[2] });
blob[BUFFER] = slicedBuffer;
return blob;
}
}
Object.defineProperties(Blob.prototype, {
size: { enumerable: true },
type: { enumerable: true },
slice: { enumerable: true }
});
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
value: 'Blob',
writable: false,
enumerable: false,
configurable: true
});
/**
* fetch-error.js
*
* FetchError interface for operational errors
*/
/**
* Create FetchError instance
*
* @param String message Error message for human
* @param String type Error type for machine
* @param String systemError For Node.js system error
* @return FetchError
*/
function FetchError(message, type, systemError) {
Error.call(this, message);
this.message = message;
this.type = type;
// when err.type is `system`, err.code contains system error code
if (systemError) {
this.code = this.errno = systemError.code;
}
// hide custom error implementation details from end-users
Error.captureStackTrace(this, this.constructor);
}
FetchError.prototype = Object.create(Error.prototype);
FetchError.prototype.constructor = FetchError;
FetchError.prototype.name = 'FetchError';
let convert;
try {
convert = require('encoding').convert;
} catch (e) {}
const INTERNALS = Symbol('Body internals');
// fix an issue where "PassThrough" isn't a named export for node <10
const PassThrough = Stream.PassThrough;
/**
* Body mixin
*
* Ref: https://fetch.spec.whatwg.org/#body
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
function Body(body) {
var _this = this;
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$size = _ref.size;
let size = _ref$size === undefined ? 0 : _ref$size;
var _ref$timeout = _ref.timeout;
let timeout = _ref$timeout === undefined ? 0 : _ref$timeout;
if (body == null) {
// body is undefined or null
body = null;
} else if (typeof body === 'string') ; else if (isURLSearchParams(body)) ; else if (body instanceof Blob) ; else if (Buffer.isBuffer(body)) ; else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') ; else if (ArrayBuffer.isView(body)) ; else if (body instanceof Stream) ; else {
// none of the above
// coerce to string
body = String(body);
}
this[INTERNALS] = {
body,
disturbed: false,
error: null
};
this.size = size;
this.timeout = timeout;
if (body instanceof Stream) {
body.on('error', function (err) {
const error = err.name === 'AbortError' ? err : new FetchError(`Invalid response body while trying to fetch ${_this.url}: ${err.message}`, 'system', err);
_this[INTERNALS].error = error;
});
}
}
Body.prototype = {
get body() {
return this[INTERNALS].body;
},
get bodyUsed() {
return this[INTERNALS].disturbed;
},
/**
* Decode response as ArrayBuffer
*
* @return Promise
*/
arrayBuffer() {
return consumeBody.call(this).then(function (buf) {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
});
},
/**
* Return raw response as Blob
*
* @return Promise
*/
blob() {
let ct = this.headers && this.headers.get('content-type') || '';
return consumeBody.call(this).then(function (buf) {
return Object.assign(
// Prevent copying
new Blob([], {
type: ct.toLowerCase()
}), {
[BUFFER]: buf
});
});
},
/**
* Decode response as json
*
* @return Promise
*/
json() {
var _this2 = this;
return consumeBody.call(this).then(function (buffer) {
try {
return JSON.parse(buffer.toString());
} catch (err) {
return Body.Promise.reject(new FetchError(`invalid json response body at ${_this2.url} reason: ${err.message}`, 'invalid-json'));
}
});
},
/**
* Decode response as text
*
* @return Promise
*/
text() {
return consumeBody.call(this).then(function (buffer) {
return buffer.toString();
});
},
/**
* Decode response as buffer (non-spec api)
*
* @return Promise
*/
buffer() {
return consumeBody.call(this);
},
/**
* Decode response as text, while automatically detecting the encoding and
* trying to decode to UTF-8 (non-spec api)
*
* @return Promise
*/
textConverted() {
var _this3 = this;
return consumeBody.call(this).then(function (buffer) {
return convertBody(buffer, _this3.headers);
});
}
};
// In browsers, all properties are enumerable.
Object.defineProperties(Body.prototype, {
body: { enumerable: true },
bodyUsed: { enumerable: true },
arrayBuffer: { enumerable: true },
blob: { enumerable: true },
json: { enumerable: true },
text: { enumerable: true }
});
Body.mixIn = function (proto) {
for (const name of Object.getOwnPropertyNames(Body.prototype)) {
// istanbul ignore else: future proof
if (!(name in proto)) {
const desc = Object.getOwnPropertyDescriptor(Body.prototype, name);
Object.defineProperty(proto, name, desc);
}
}
};
/**
* Consume and convert an entire Body to a Buffer.
*
* Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
*
* @return Promise
*/
function consumeBody() {
var _this4 = this;
if (this[INTERNALS].disturbed) {
return Body.Promise.reject(new TypeError(`body used already for: ${this.url}`));
}
this[INTERNALS].disturbed = true;
if (this[INTERNALS].error) {
return Body.Promise.reject(this[INTERNALS].error);
}
// body is null
if (this.body === null) {
return Body.Promise.resolve(Buffer.alloc(0));
}
// body is string
if (typeof this.body === 'string') {
return Body.Promise.resolve(Buffer.from(this.body));
}
// body is blob
if (this.body instanceof Blob) {
return Body.Promise.resolve(this.body[BUFFER]);
}
// body is buffer
if (Buffer.isBuffer(this.body)) {
return Body.Promise.resolve(this.body);
}
// body is ArrayBuffer
if (Object.prototype.toString.call(this.body) === '[object ArrayBuffer]') {
return Body.Promise.resolve(Buffer.from(this.body));
}
// body is ArrayBufferView
if (ArrayBuffer.isView(this.body)) {
return Body.Promise.resolve(Buffer.from(this.body.buffer, this.body.byteOffset, this.body.byteLength));
}
// istanbul ignore if: should never happen
if (!(this.body instanceof Stream)) {
return Body.Promise.resolve(Buffer.alloc(0));
}
// body is stream
// get ready to actually consume the body
let accum = [];
let accumBytes = 0;
let abort = false;
return new Body.Promise(function (resolve, reject) {
let resTimeout;
// allow timeout on slow response body
if (_this4.timeout) {
resTimeout = setTimeout(function () {
abort = true;
reject(new FetchError(`Response timeout while trying to fetch ${_this4.url} (over ${_this4.timeout}ms)`, 'body-timeout'));
}, _this4.timeout);
}
// handle stream errors
_this4.body.on('error', function (err) {
if (err.name === 'AbortError') {
// if the request was aborted, reject with this Error
abort = true;
reject(err);
} else {
// other errors, such as incorrect content-encoding
reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err));
}
});
_this4.body.on('data', function (chunk) {
if (abort || chunk === null) {
return;
}
if (_this4.size && accumBytes + chunk.length > _this4.size) {
abort = true;
reject(new FetchError(`content size at ${_this4.url} over limit: ${_this4.size}`, 'max-size'));
return;
}
accumBytes += chunk.length;
accum.push(chunk);
});
_this4.body.on('end', function () {
if (abort) {
return;
}
clearTimeout(resTimeout);
try {
resolve(Buffer.concat(accum));
} catch (err) {
// handle streams that have accumulated too much data (issue #414)
reject(new FetchError(`Could not create Buffer from response body for ${_this4.url}: ${err.message}`, 'system', err));
}
});
});
}
/**
* Detect buffer encoding and convert to target encoding
* ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
*
* @param Buffer buffer Incoming buffer
* @param String encoding Target encoding
* @return String
*/
function convertBody(buffer, headers) {
if (typeof convert !== 'function') {
throw new Error('The package `encoding` must be installed to use the textConverted() function');
}
const ct = headers.get('content-type');
let charset = 'utf-8';
let res, str;
// header
if (ct) {
res = /charset=([^;]*)/i.exec(ct);
}
// no charset in content type, peek at response body for at most 1024 bytes
str = buffer.slice(0, 1024).toString();
// html5
if (!res && str) {
res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str);
}
// html4
if (!res && str) {
res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str);
if (res) {
res = /charset=(.*)/i.exec(res.pop());
}
}
// xml
if (!res && str) {
res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str);
}
// found charset
if (res) {
charset = res.pop();
// prevent decode issues when sites use incorrect encoding
// ref: https://hsivonen.fi/encoding-menu/
if (charset === 'gb2312' || charset === 'gbk') {
charset = 'gb18030';
}
}
// turn raw buffers into a single utf-8 buffer
return convert(buffer, 'UTF-8', charset).toString();
}
/**
* Detect a URLSearchParams object
* ref: https://github.com/bitinn/node-fetch/issues/296#issuecomment-307598143
*
* @param Object obj Object to detect by type or brand
* @return String
*/
function isURLSearchParams(obj) {
// Duck-typing as a necessary condition.
if (typeof obj !== 'object' || typeof obj.append !== 'function' || typeof obj.delete !== 'function' || typeof obj.get !== 'function' || typeof obj.getAll !== 'function' || typeof obj.has !== 'function' || typeof obj.set !== 'function') {
return false;
}
// Brand-checking and more duck-typing as optional condition.
return obj.constructor.name === 'URLSearchParams' || Object.prototype.toString.call(obj) === '[object URLSearchParams]' || typeof obj.sort === 'function';
}
/**
* Clone body given Res/Req instance
*
* @param Mixed instance Response or Request instance
* @return Mixed
*/
function clone(instance) {
let p1, p2;
let body = instance.body;
// don't allow cloning a used body
if (instance.bodyUsed) {
throw new Error('cannot clone body after it is used');
}
// check that body is a stream and not form-data object
// note: we can't clone the form-data object without having it as a dependency
if (body instanceof Stream && typeof body.getBoundary !== 'function') {
// tee instance body
p1 = new PassThrough();
p2 = new PassThrough();
body.pipe(p1);
body.pipe(p2);
// set instance body to teed body and return the other teed body
instance[INTERNALS].body = p1;
body = p2;
}
return body;
}
/**
* Performs the operation "extract a `Content-Type` value from |object|" as
* specified in the specification:
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract
*
* This function assumes that instance.body is present.
*
* @param Mixed instance Response or Request instance
*/
function extractContentType(instance) {
const body = instance.body;
// istanbul ignore if: Currently, because of a guard in Request, body
// can never be null. Included here for completeness.
if (body === null) {
// body is null
return null;
} else if (typeof body === 'string') {
// body is string
return 'text/plain;charset=UTF-8';
} else if (isURLSearchParams(body)) {
// body is a URLSearchParams
return 'application/x-www-form-urlencoded;charset=UTF-8';
} else if (body instanceof Blob) {
// body is blob
return body.type || null;
} else if (Buffer.isBuffer(body)) {
// body is buffer
return null;
} else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') {
// body is ArrayBuffer
return null;
} else if (ArrayBuffer.isView(body)) {
// body is ArrayBufferView
return null;
} else if (typeof body.getBoundary === 'function') {
// detect form data input from form-data module
return `multipart/form-data;boundary=${body.getBoundary()}`;
} else {
// body is stream
// can't really do much about this
return null;
}
}
/**
* The Fetch Standard treats this as if "total bytes" is a property on the body.
* For us, we have to explicitly get it with a function.
*
* ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
*
* @param Body instance Instance of Body
* @return Number? Number of bytes, or null if not possible
*/
function getTotalBytes(instance) {
const body = instance.body;
// istanbul ignore if: included for completion
if (body === null) {
// body is null
return 0;
} else if (typeof body === 'string') {
// body is string
return Buffer.byteLength(body);
} else if (isURLSearchParams(body)) {
// body is URLSearchParams
return Buffer.byteLength(String(body));
} else if (body instanceof Blob) {
// body is blob
return body.size;
} else if (Buffer.isBuffer(body)) {
// body is buffer
return body.length;
} else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') {
// body is ArrayBuffer
return body.byteLength;
} else if (ArrayBuffer.isView(body)) {
// body is ArrayBufferView
return body.byteLength;
} else if (body && typeof body.getLengthSync === 'function') {
// detect form data input from form-data module
if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x
body.hasKnownLength && body.hasKnownLength()) {
// 2.x
return body.getLengthSync();
}
return null;
} else {
// body is stream
// can't really do much about this
return null;
}
}
/**
* Write a Body to a Node.js WritableStream (e.g. http.Request) object.
*
* @param Body instance Instance of Body
* @return Void
*/
function writeToStream(dest, instance) {
const body = instance.body;
if (body === null) {
// body is null
dest.end();
} else if (typeof body === 'string') {
// body is string
dest.write(body);
dest.end();
} else if (isURLSearchParams(body)) {
// body is URLSearchParams
dest.write(Buffer.from(String(body)));
dest.end();
} else if (body instanceof Blob) {
// body is blob
dest.write(body[BUFFER]);
dest.end();
} else if (Buffer.isBuffer(body)) {
// body is buffer
dest.write(body);
dest.end();
} else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') {
// body is ArrayBuffer
dest.write(Buffer.from(body));
dest.end();
} else if (ArrayBuffer.isView(body)) {
// body is ArrayBufferView
dest.write(Buffer.from(body.buffer, body.byteOffset, body.byteLength));
dest.end();
} else {
// body is stream
body.pipe(dest);
}
}
// expose Promise
Body.Promise = global.Promise;
/**
* headers.js
*
* Headers class offers convenient helpers
*/
const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
function validateName(name) {
name = `${name}`;
if (invalidTokenRegex.test(name)) {
throw new TypeError(`${name} is not a legal HTTP header name`);
}
}
function validateValue(value) {
value = `${value}`;
if (invalidHeaderCharRegex.test(value)) {
throw new TypeError(`${value} is not a legal HTTP header value`);
}
}
/**
* Find the key in the map object given a header name.
*
* Returns undefined if not found.
*
* @param String name Header name
* @return String|Undefined
*/
function find(map, name) {
name = name.toLowerCase();
for (const key in map) {
if (key.toLowerCase() === name) {
return key;
}
}
return undefined;
}
const MAP = Symbol('map');
class Headers {
/**
* Headers class
*
* @param Object headers Response headers
* @return Void
*/
constructor() {
let init = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined;
this[MAP] = Object.create(null);
if (init instanceof Headers) {
const rawHeaders = init.raw();
const headerNames = Object.keys(rawHeaders);
for (const headerName of headerNames) {
for (const value of rawHeaders[headerName]) {
this.append(headerName, value);
}
}
return;
}
// We don't worry about converting prop to ByteString here as append()
// will handle it.
if (init == null) ; else if (typeof init === 'object') {
const method = init[Symbol.iterator];
if (method != null) {
if (typeof method !== 'function') {
throw new TypeError('Header pairs must be iterable');
}
// sequence<sequence<ByteString>>
// Note: per spec we have to first exhaust the lists then process them
const pairs = [];
for (const pair of init) {
if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') {
throw new TypeError('Each header pair must be iterable');
}
pairs.push(Array.from(pair));
}
for (const pair of pairs) {
if (pair.length !== 2) {
throw new TypeError('Each header pair must be a name/value tuple');
}
this.append(pair[0], pair[1]);
}
} else {
// record<ByteString, ByteString>
for (const key of Object.keys(init)) {
const value = init[key];
this.append(key, value);
}
}
} else {
throw new TypeError('Provided initializer must be an object');
}
}
/**
* Return combined header value given name
*
* @param String name Header name
* @return Mixed
*/
get(name) {
name = `${name}`;
validateName(name);
const key = find(this[MAP], name);
if (key === undefined) {
return null;
}
return this[MAP][key].join(', ');
}
/**
* Iterate over all headers
*
* @param Function callback Executed for each item with parameters (value, name, thisArg)
* @param Boolean thisArg `this` context for callback function
* @return Void
*/
forEach(callback) {
let thisArg = arguments.length > 1 && ar