hk-address-parser-lib
Version:
The library for the HKAddressParser project
1,646 lines (1,440 loc) • 694 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@turf/turf'), require('proj4')) :
typeof define === 'function' && define.amd ? define(['exports', '@turf/turf', 'proj4'], factory) :
(global = global || self, factory(global['hk-address-parser'] = {}, global.turf, global.proj4));
}(this, function (exports, turf, proj4) { 'use strict';
proj4 = proj4 && proj4.hasOwnProperty('default') ? proj4['default'] : proj4;
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;
}
var domain;
// This constructor is used to store event handlers. Instantiating this is
// faster than explicitly calling `Object.create(null)` to get a "clean" empty
// object (tested with v8 v4.9).
function EventHandlers() {}
EventHandlers.prototype = Object.create(null);
function EventEmitter() {
EventEmitter.init.call(this);
}
// nodejs oddity
// require('events') === require('events').EventEmitter
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.usingDomains = false;
EventEmitter.prototype.domain = undefined;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._maxListeners = undefined;
// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
EventEmitter.defaultMaxListeners = 10;
EventEmitter.init = function() {
this.domain = null;
if (EventEmitter.usingDomains) {
// if there is an active domain, then attach to it.
if (domain.active && !(this instanceof domain.Domain)) ;
}
if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
this._events = new EventHandlers();
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined;
};
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
if (typeof n !== 'number' || n < 0 || isNaN(n))
throw new TypeError('"n" argument must be a positive number');
this._maxListeners = n;
return this;
};
function $getMaxListeners(that) {
if (that._maxListeners === undefined)
return EventEmitter.defaultMaxListeners;
return that._maxListeners;
}
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
return $getMaxListeners(this);
};
// These standalone emit* functions are used to optimize calling of event
// handlers for fast cases because emit() itself often has a variable number of
// arguments and can be deoptimized because of that. These functions always have
// the same number of arguments and thus do not get deoptimized, so the code
// inside them can execute faster.
function emitNone(handler, isFn, self) {
if (isFn)
handler.call(self);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self);
}
}
function emitOne(handler, isFn, self, arg1) {
if (isFn)
handler.call(self, arg1);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1);
}
}
function emitTwo(handler, isFn, self, arg1, arg2) {
if (isFn)
handler.call(self, arg1, arg2);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1, arg2);
}
}
function emitThree(handler, isFn, self, arg1, arg2, arg3) {
if (isFn)
handler.call(self, arg1, arg2, arg3);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1, arg2, arg3);
}
}
function emitMany(handler, isFn, self, args) {
if (isFn)
handler.apply(self, args);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].apply(self, args);
}
}
EventEmitter.prototype.emit = function emit(type) {
var er, handler, len, args, i, events, domain;
var doError = (type === 'error');
events = this._events;
if (events)
doError = (doError && events.error == null);
else if (!doError)
return false;
domain = this.domain;
// If there is no 'error' event listener then throw.
if (doError) {
er = arguments[1];
if (domain) {
if (!er)
er = new Error('Uncaught, unspecified "error" event');
er.domainEmitter = this;
er.domain = domain;
er.domainThrown = false;
domain.emit('error', er);
} else if (er instanceof Error) {
throw er; // Unhandled 'error' event
} else {
// At least give some kind of context to the user
var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
err.context = er;
throw err;
}
return false;
}
handler = events[type];
if (!handler)
return false;
var isFn = typeof handler === 'function';
len = arguments.length;
switch (len) {
// fast cases
case 1:
emitNone(handler, isFn, this);
break;
case 2:
emitOne(handler, isFn, this, arguments[1]);
break;
case 3:
emitTwo(handler, isFn, this, arguments[1], arguments[2]);
break;
case 4:
emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
break;
// slower
default:
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
emitMany(handler, isFn, this, args);
}
return true;
};
function _addListener(target, type, listener, prepend) {
var m;
var events;
var existing;
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
events = target._events;
if (!events) {
events = target._events = new EventHandlers();
target._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener) {
target.emit('newListener', type,
listener.listener ? listener.listener : listener);
// Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = target._events;
}
existing = events[type];
}
if (!existing) {
// Optimize the case of one listener. Don't need the extra array object.
existing = events[type] = listener;
++target._eventsCount;
} else {
if (typeof existing === 'function') {
// Adding the second element, need to change to array.
existing = events[type] = prepend ? [listener, existing] :
[existing, listener];
} else {
// If we've already got an array, just append.
if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
}
// Check for listener leak
if (!existing.warned) {
m = $getMaxListeners(target);
if (m && m > 0 && existing.length > m) {
existing.warned = true;
var w = new Error('Possible EventEmitter memory leak detected. ' +
existing.length + ' ' + type + ' listeners added. ' +
'Use emitter.setMaxListeners() to increase limit');
w.name = 'MaxListenersExceededWarning';
w.emitter = target;
w.type = type;
w.count = existing.length;
emitWarning(w);
}
}
}
return target;
}
function emitWarning(e) {
typeof console.warn === 'function' ? console.warn(e) : console.log(e);
}
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
return _addListener(this, type, listener, true);
};
function _onceWrap(target, type, listener) {
var fired = false;
function g() {
target.removeListener(type, g);
if (!fired) {
fired = true;
listener.apply(target, arguments);
}
}
g.listener = listener;
return g;
}
EventEmitter.prototype.once = function once(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
this.on(type, _onceWrap(this, type, listener));
return this;
};
EventEmitter.prototype.prependOnceListener =
function prependOnceListener(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
this.prependListener(type, _onceWrap(this, type, listener));
return this;
};
// emits a 'removeListener' event iff the listener was removed
EventEmitter.prototype.removeListener =
function removeListener(type, listener) {
var list, events, position, i, originalListener;
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
events = this._events;
if (!events)
return this;
list = events[type];
if (!list)
return this;
if (list === listener || (list.listener && list.listener === listener)) {
if (--this._eventsCount === 0)
this._events = new EventHandlers();
else {
delete events[type];
if (events.removeListener)
this.emit('removeListener', type, list.listener || listener);
}
} else if (typeof list !== 'function') {
position = -1;
for (i = list.length; i-- > 0;) {
if (list[i] === listener ||
(list[i].listener && list[i].listener === listener)) {
originalListener = list[i].listener;
position = i;
break;
}
}
if (position < 0)
return this;
if (list.length === 1) {
list[0] = undefined;
if (--this._eventsCount === 0) {
this._events = new EventHandlers();
return this;
} else {
delete events[type];
}
} else {
spliceOne(list, position);
}
if (events.removeListener)
this.emit('removeListener', type, originalListener || listener);
}
return this;
};
EventEmitter.prototype.removeAllListeners =
function removeAllListeners(type) {
var listeners, events;
events = this._events;
if (!events)
return this;
// not listening for removeListener, no need to emit
if (!events.removeListener) {
if (arguments.length === 0) {
this._events = new EventHandlers();
this._eventsCount = 0;
} else if (events[type]) {
if (--this._eventsCount === 0)
this._events = new EventHandlers();
else
delete events[type];
}
return this;
}
// emit removeListener for all listeners on all events
if (arguments.length === 0) {
var keys = Object.keys(events);
for (var i = 0, key; i < keys.length; ++i) {
key = keys[i];
if (key === 'removeListener') continue;
this.removeAllListeners(key);
}
this.removeAllListeners('removeListener');
this._events = new EventHandlers();
this._eventsCount = 0;
return this;
}
listeners = events[type];
if (typeof listeners === 'function') {
this.removeListener(type, listeners);
} else if (listeners) {
// LIFO order
do {
this.removeListener(type, listeners[listeners.length - 1]);
} while (listeners[0]);
}
return this;
};
EventEmitter.prototype.listeners = function listeners(type) {
var evlistener;
var ret;
var events = this._events;
if (!events)
ret = [];
else {
evlistener = events[type];
if (!evlistener)
ret = [];
else if (typeof evlistener === 'function')
ret = [evlistener.listener || evlistener];
else
ret = unwrapListeners(evlistener);
}
return ret;
};
EventEmitter.listenerCount = function(emitter, type) {
if (typeof emitter.listenerCount === 'function') {
return emitter.listenerCount(type);
} else {
return listenerCount.call(emitter, type);
}
};
EventEmitter.prototype.listenerCount = listenerCount;
function listenerCount(type) {
var events = this._events;
if (events) {
var evlistener = events[type];
if (typeof evlistener === 'function') {
return 1;
} else if (evlistener) {
return evlistener.length;
}
}
return 0;
}
EventEmitter.prototype.eventNames = function eventNames() {
return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
};
// About 1.5x faster than the two-arg version of Array#splice().
function spliceOne(list, index) {
for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
list[i] = list[k];
list.pop();
}
function arrayClone(arr, i) {
var copy = new Array(i);
while (i--)
copy[i] = arr[i];
return copy;
}
function unwrapListeners(arr) {
var ret = new Array(arr.length);
for (var i = 0; i < ret.length; ++i) {
ret[i] = arr[i].listener || arr[i];
}
return ret;
}
// shim for using process in browser
// based off https://github.com/defunctzombie/node-process/blob/master/browser.js
function defaultSetTimout() {
throw new Error('setTimeout has not been defined');
}
function defaultClearTimeout () {
throw new Error('clearTimeout has not been defined');
}
var cachedSetTimeout = defaultSetTimout;
var cachedClearTimeout = defaultClearTimeout;
if (typeof global.setTimeout === 'function') {
cachedSetTimeout = setTimeout;
}
if (typeof global.clearTimeout === 'function') {
cachedClearTimeout = clearTimeout;
}
function runTimeout(fun) {
if (cachedSetTimeout === setTimeout) {
//normal enviroments in sane situations
return setTimeout(fun, 0);
}
// if setTimeout wasn't available but was latter defined
if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
cachedSetTimeout = setTimeout;
return setTimeout(fun, 0);
}
try {
// when when somebody has screwed with setTimeout but no I.E. maddness
return cachedSetTimeout(fun, 0);
} catch(e){
try {
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
return cachedSetTimeout.call(null, fun, 0);
} catch(e){
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
return cachedSetTimeout.call(this, fun, 0);
}
}
}
function runClearTimeout(marker) {
if (cachedClearTimeout === clearTimeout) {
//normal enviroments in sane situations
return clearTimeout(marker);
}
// if clearTimeout wasn't available but was latter defined
if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
cachedClearTimeout = clearTimeout;
return clearTimeout(marker);
}
try {
// when when somebody has screwed with setTimeout but no I.E. maddness
return cachedClearTimeout(marker);
} catch (e){
try {
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
return cachedClearTimeout.call(null, marker);
} catch (e){
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
// Some versions of I.E. have different rules for clearTimeout vs setTimeout
return cachedClearTimeout.call(this, marker);
}
}
}
var queue = [];
var draining = false;
var currentQueue;
var queueIndex = -1;
function cleanUpNextTick() {
if (!draining || !currentQueue) {
return;
}
draining = false;
if (currentQueue.length) {
queue = currentQueue.concat(queue);
} else {
queueIndex = -1;
}
if (queue.length) {
drainQueue();
}
}
function drainQueue() {
if (draining) {
return;
}
var timeout = runTimeout(cleanUpNextTick);
draining = true;
var len = queue.length;
while(len) {
currentQueue = queue;
queue = [];
while (++queueIndex < len) {
if (currentQueue) {
currentQueue[queueIndex].run();
}
}
queueIndex = -1;
len = queue.length;
}
currentQueue = null;
draining = false;
runClearTimeout(timeout);
}
function nextTick(fun) {
var args = new Array(arguments.length - 1);
if (arguments.length > 1) {
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
}
queue.push(new Item(fun, args));
if (queue.length === 1 && !draining) {
runTimeout(drainQueue);
}
}
// v8 likes predictible objects
function Item(fun, array) {
this.fun = fun;
this.array = array;
}
Item.prototype.run = function () {
this.fun.apply(null, this.array);
};
var title = 'browse