5 require_once(CONST_BasePath.'/lib/NearPoint.php');
6 require_once(CONST_BasePath.'/lib/PlaceLookup.php');
7 require_once(CONST_BasePath.'/lib/ReverseGeocode.php');
13 protected $aLangPrefOrder = array();
15 protected $bIncludeAddressDetails = false;
16 protected $bIncludeExtraTags = false;
17 protected $bIncludeNameDetails = false;
19 protected $bIncludePolygonAsPoints = false;
20 protected $bIncludePolygonAsText = false;
21 protected $bIncludePolygonAsGeoJSON = false;
22 protected $bIncludePolygonAsKML = false;
23 protected $bIncludePolygonAsSVG = false;
24 protected $fPolygonSimplificationThreshold = 0.0;
26 protected $aExcludePlaceIDs = array();
27 protected $bDeDupe = true;
28 protected $bReverseInPlan = false;
30 protected $iLimit = 20;
31 protected $iFinalLimit = 10;
32 protected $iOffset = 0;
33 protected $bFallback = false;
35 protected $aCountryCodes = false;
37 protected $bBoundedSearch = false;
38 protected $aViewBox = false;
39 protected $sViewboxCentreSQL = false;
40 protected $sViewboxSmallSQL = false;
41 protected $sViewboxLargeSQL = false;
43 protected $iMaxRank = 20;
44 protected $iMinAddressRank = 0;
45 protected $iMaxAddressRank = 30;
46 protected $aAddressRankList = array();
47 protected $exactMatchCache = array();
49 protected $sAllowedTypesSQLList = false;
51 protected $sQuery = false;
52 protected $aStructuredQuery = false;
55 public function __construct(&$oDB)
60 public function setReverseInPlan($bReverse)
62 $this->bReverseInPlan = $bReverse;
65 public function setLanguagePreference($aLangPref)
67 $this->aLangPrefOrder = $aLangPref;
70 public function getMoreUrlParams()
72 if ($this->aStructuredQuery) {
73 $aParams = $this->aStructuredQuery;
75 $aParams = array('q' => $this->sQuery);
78 if ($this->aExcludePlaceIDs) {
79 $aParams['exclude_place_ids'] = implode(',', $this->aExcludePlaceIDs);
82 if ($this->bIncludeAddressDetails) $aParams['addressdetails'] = '1';
83 if ($this->bIncludeExtraTags) $aParams['extratags'] = '1';
84 if ($this->bIncludeNameDetails) $aParams['namedetails'] = '1';
86 if ($this->bIncludePolygonAsPoints) $aParams['polygon'] = '1';
87 if ($this->bIncludePolygonAsText) $aParams['polygon_text'] = '1';
88 if ($this->bIncludePolygonAsGeoJSON) $aParams['polygon_geojson'] = '1';
89 if ($this->bIncludePolygonAsKML) $aParams['polygon_kml'] = '1';
90 if ($this->bIncludePolygonAsSVG) $aParams['polygon_svg'] = '1';
92 if ($this->fPolygonSimplificationThreshold > 0.0) {
93 $aParams['polygon_threshold'] = $this->fPolygonSimplificationThreshold;
96 if ($this->bBoundedSearch) $aParams['bounded'] = '1';
97 if (!$this->bDeDupe) $aParams['dedupe'] = '0';
99 if ($this->aCountryCodes) {
100 $aParams['countrycodes'] = implode(',', $this->aCountryCodes);
103 if ($this->aViewBox) {
104 $aParams['viewbox'] = $this->aViewBox[0].','.$this->aViewBox[3]
105 .','.$this->aViewBox[2].','.$this->aViewBox[1];
111 public function setIncludePolygonAsPoints($b = true)
113 $this->bIncludePolygonAsPoints = $b;
116 public function setIncludePolygonAsText($b = true)
118 $this->bIncludePolygonAsText = $b;
121 public function setIncludePolygonAsGeoJSON($b = true)
123 $this->bIncludePolygonAsGeoJSON = $b;
126 public function setIncludePolygonAsKML($b = true)
128 $this->bIncludePolygonAsKML = $b;
131 public function setIncludePolygonAsSVG($b = true)
133 $this->bIncludePolygonAsSVG = $b;
136 public function setPolygonSimplificationThreshold($f)
138 $this->fPolygonSimplificationThreshold = $f;
141 public function setLimit($iLimit = 10)
143 if ($iLimit > 50) $iLimit = 50;
144 if ($iLimit < 1) $iLimit = 1;
146 $this->iFinalLimit = $iLimit;
147 $this->iLimit = $iLimit + min($iLimit, 10);
150 public function setFeatureType($sFeatureType)
152 switch ($sFeatureType) {
154 $this->setRankRange(4, 4);
157 $this->setRankRange(8, 8);
160 $this->setRankRange(14, 16);
163 $this->setRankRange(8, 20);
168 public function setRankRange($iMin, $iMax)
170 $this->iMinAddressRank = $iMin;
171 $this->iMaxAddressRank = $iMax;
174 public function setRoute($aRoutePoints, $fRouteWidth)
176 $this->aViewBox = false;
178 $this->sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
180 foreach ($aRoutePoints as $aPoint) {
181 $fPoint = (float)$aPoint;
182 $this->sViewboxCentreSQL .= $sSep.$fPoint;
183 $sSep = ($sSep == ' ') ? ',' : ' ';
185 $this->sViewboxCentreSQL .= ")'::geometry,4326)";
187 $this->sViewboxSmallSQL = 'ST_BUFFER('.$this->sViewboxCentreSQL;
188 $this->sViewboxSmallSQL .= ','.($fRouteWidth/69).')';
190 $this->sViewboxLargeSQL = 'ST_BUFFER('.$this->sViewboxCentreSQL;
191 $this->sViewboxLargeSQL .= ','.($fRouteWidth/30).')';
194 public function setViewbox($aViewbox)
196 $this->aViewBox = array_map('floatval', $aViewbox);
198 $this->aViewBox[0] = max(-180.0, min(180, $this->aViewBox[0]));
199 $this->aViewBox[1] = max(-90.0, min(90, $this->aViewBox[1]));
200 $this->aViewBox[2] = max(-180.0, min(180, $this->aViewBox[2]));
201 $this->aViewBox[3] = max(-90.0, min(90, $this->aViewBox[3]));
203 if (abs($this->aViewBox[0] - $this->aViewBox[2]) < 0.000000001
204 || abs($this->aViewBox[1] - $this->aViewBox[3]) < 0.000000001
206 userError("Bad parameter 'viewbox'. Not a box.");
209 $fHeight = $this->aViewBox[0] - $this->aViewBox[2];
210 $fWidth = $this->aViewBox[1] - $this->aViewBox[3];
211 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
212 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
213 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
214 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
216 $this->sViewboxCentreSQL = false;
217 $this->sViewboxSmallSQL = sprintf(
218 'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
224 $this->sViewboxLargeSQL = sprintf(
225 'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
233 public function setQuery($sQueryString)
235 $this->sQuery = $sQueryString;
236 $this->aStructuredQuery = false;
239 public function getQueryString()
241 return $this->sQuery;
245 public function loadParamArray($oParams)
247 $this->bIncludeAddressDetails
248 = $oParams->getBool('addressdetails', $this->bIncludeAddressDetails);
249 $this->bIncludeExtraTags
250 = $oParams->getBool('extratags', $this->bIncludeExtraTags);
251 $this->bIncludeNameDetails
252 = $oParams->getBool('namedetails', $this->bIncludeNameDetails);
254 $this->bBoundedSearch = $oParams->getBool('bounded', $this->bBoundedSearch);
255 $this->bDeDupe = $oParams->getBool('dedupe', $this->bDeDupe);
257 $this->setLimit($oParams->getInt('limit', $this->iFinalLimit));
258 $this->iOffset = $oParams->getInt('offset', $this->iOffset);
260 $this->bFallback = $oParams->getBool('fallback', $this->bFallback);
262 // List of excluded Place IDs - used for more acurate pageing
263 $sExcluded = $oParams->getStringList('exclude_place_ids');
265 foreach ($sExcluded as $iExcludedPlaceID) {
266 $iExcludedPlaceID = (int)$iExcludedPlaceID;
267 if ($iExcludedPlaceID)
268 $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
271 if (isset($aExcludePlaceIDs))
272 $this->aExcludePlaceIDs = $aExcludePlaceIDs;
275 // Only certain ranks of feature
276 $sFeatureType = $oParams->getString('featureType');
277 if (!$sFeatureType) $sFeatureType = $oParams->getString('featuretype');
278 if ($sFeatureType) $this->setFeatureType($sFeatureType);
281 $sCountries = $oParams->getStringList('countrycodes');
283 foreach ($sCountries as $sCountryCode) {
284 if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode)) {
285 $aCountries[] = strtolower($sCountryCode);
288 if (isset($aCountries))
289 $this->aCountryCodes = $aCountries;
292 $aViewbox = $oParams->getStringList('viewboxlbrt');
294 if (count($aViewbox) != 4) {
295 userError("Bad parmater 'viewbox'. Expected 4 coordinates.");
297 $this->setViewbox($aViewbox);
299 $aViewbox = $oParams->getStringList('viewbox');
301 if (count($aViewbox) != 4) {
302 userError("Bad parmater 'viewbox'. Expected 4 coordinates.");
304 $this->setViewBox(array(
311 $aRoute = $oParams->getStringList('route');
312 $fRouteWidth = $oParams->getFloat('routewidth');
313 if ($aRoute && $fRouteWidth) {
314 $this->setRoute($aRoute, $fRouteWidth);
320 public function setQueryFromParams($oParams)
323 $sQuery = $oParams->getString('q');
325 $this->setStructuredQuery(
326 $oParams->getString('amenity'),
327 $oParams->getString('street'),
328 $oParams->getString('city'),
329 $oParams->getString('county'),
330 $oParams->getString('state'),
331 $oParams->getString('country'),
332 $oParams->getString('postalcode')
334 $this->setReverseInPlan(false);
336 $this->setQuery($sQuery);
340 public function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
342 $sValue = trim($sValue);
343 if (!$sValue) return false;
344 $this->aStructuredQuery[$sKey] = $sValue;
345 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30) {
346 $this->iMinAddressRank = $iNewMinAddressRank;
347 $this->iMaxAddressRank = $iNewMaxAddressRank;
349 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
353 public function setStructuredQuery($sAmenity = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
355 $this->sQuery = false;
358 $this->iMinAddressRank = 0;
359 $this->iMaxAddressRank = 30;
360 $this->aAddressRankList = array();
362 $this->aStructuredQuery = array();
363 $this->sAllowedTypesSQLList = '';
365 $this->loadStructuredAddressElement($sAmenity, 'amenity', 26, 30, false);
366 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
367 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
368 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
369 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
370 $this->loadStructuredAddressElement($sPostalCode, 'postalcode', 5, 11, array(5, 11));
371 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
373 if (sizeof($this->aStructuredQuery) > 0) {
374 $this->sQuery = join(', ', $this->aStructuredQuery);
375 if ($this->iMaxAddressRank < 30) {
376 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
381 public function fallbackStructuredQuery()
383 if (!$this->aStructuredQuery) return false;
385 $aParams = $this->aStructuredQuery;
387 if (sizeof($aParams) == 1) return false;
389 $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
391 foreach ($aOrderToFallback as $sType) {
392 if (isset($aParams[$sType])) {
393 unset($aParams[$sType]);
394 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
402 public function getDetails($aPlaceIDs)
404 //$aPlaceIDs is an array with key: placeID and value: tiger-housenumber, if found, else -1
405 if (sizeof($aPlaceIDs) == 0) return array();
407 $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
409 // Get the details for display (is this a redundant extra step?)
410 $sPlaceIDs = join(',', array_keys($aPlaceIDs));
412 $sImportanceSQL = '';
413 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
414 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
417 $sSQL .= " osm_type,";
421 $sSQL .= " admin_level,";
422 $sSQL .= " rank_search,";
423 $sSQL .= " rank_address,";
424 $sSQL .= " min(place_id) AS place_id, ";
425 $sSQL .= " min(parent_place_id) AS parent_place_id, ";
426 $sSQL .= " country_code, ";
427 $sSQL .= " get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) AS langaddress,";
428 $sSQL .= " get_name_by_language(name, $sLanguagePrefArraySQL) AS placename,";
429 $sSQL .= " get_name_by_language(name, ARRAY['ref']) AS ref,";
430 if ($this->bIncludeExtraTags) $sSQL .= "hstore_to_json(extratags)::text AS extra,";
431 if ($this->bIncludeNameDetails) $sSQL .= "hstore_to_json(name)::text AS names,";
432 $sSQL .= " avg(ST_X(centroid)) AS lon, ";
433 $sSQL .= " avg(ST_Y(centroid)) AS lat, ";
434 $sSQL .= " ".$sImportanceSQL."COALESCE(importance,0.75-(rank_search::float/40)) AS importance, ";
436 $sSQL .= " SELECT max(p.importance*(p.rank_address+2))";
438 $sSQL .= " place_addressline s, ";
439 $sSQL .= " placex p";
440 $sSQL .= " WHERE s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END)";
441 $sSQL .= " AND p.place_id = s.address_place_id ";
442 $sSQL .= " AND s.isaddress ";
443 $sSQL .= " AND p.importance is not null ";
444 $sSQL .= " ) AS addressimportance, ";
445 $sSQL .= " (extratags->'place') AS extra_place ";
446 $sSQL .= " FROM placex";
447 $sSQL .= " WHERE place_id in ($sPlaceIDs) ";
449 $sSQL .= " placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
450 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) {
451 $sSQL .= " OR (extratags->'place') = 'city'";
453 if ($this->aAddressRankList) {
454 $sSQL .= " OR placex.rank_address in (".join(',', $this->aAddressRankList).")";
457 if ($this->sAllowedTypesSQLList) {
458 $sSQL .= "AND placex.class in $this->sAllowedTypesSQLList ";
460 $sSQL .= " AND linked_place_id is null ";
461 $sSQL .= " GROUP BY ";
462 $sSQL .= " osm_type, ";
463 $sSQL .= " osm_id, ";
466 $sSQL .= " admin_level, ";
467 $sSQL .= " rank_search, ";
468 $sSQL .= " rank_address, ";
469 $sSQL .= " country_code, ";
470 $sSQL .= " importance, ";
471 if (!$this->bDeDupe) $sSQL .= "place_id,";
472 $sSQL .= " langaddress, ";
473 $sSQL .= " placename, ";
475 if ($this->bIncludeExtraTags) $sSQL .= "extratags, ";
476 if ($this->bIncludeNameDetails) $sSQL .= "name, ";
477 $sSQL .= " extratags->'place' ";
479 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank) {
480 // only Tiger housenumbers and interpolation lines need to be interpolated, because they are saved as lines
481 // with start- and endnumber, the common osm housenumbers are usually saved as points
484 $length = count($aPlaceIDs);
485 foreach ($aPlaceIDs as $placeID => $housenumber) {
487 $sHousenumbers .= "(".$placeID.", ".$housenumber.")";
488 if ($i<$length) $sHousenumbers .= ", ";
491 if (CONST_Use_US_Tiger_Data) {
492 // Tiger search only if a housenumber was searched and if it was found (i.e. aPlaceIDs[placeID] = housenumber != -1) (realized through a join)
495 $sSQL .= " 'T' AS osm_type, ";
496 $sSQL .= " (SELECT osm_id from placex p WHERE p.place_id=min(blub.parent_place_id)) as osm_id, ";
497 $sSQL .= " 'place' AS class, ";
498 $sSQL .= " 'house' AS type, ";
499 $sSQL .= " null AS admin_level, ";
500 $sSQL .= " 30 AS rank_search, ";
501 $sSQL .= " 30 AS rank_address, ";
502 $sSQL .= " min(place_id) AS place_id, ";
503 $sSQL .= " min(parent_place_id) AS parent_place_id, ";
504 $sSQL .= " 'us' AS country_code, ";
505 $sSQL .= " get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) AS langaddress,";
506 $sSQL .= " null AS placename, ";
507 $sSQL .= " null AS ref, ";
508 if ($this->bIncludeExtraTags) $sSQL .= "null AS extra,";
509 if ($this->bIncludeNameDetails) $sSQL .= "null AS names,";
510 $sSQL .= " avg(st_x(centroid)) AS lon, ";
511 $sSQL .= " avg(st_y(centroid)) AS lat,";
512 $sSQL .= " ".$sImportanceSQL."-1.15 AS importance, ";
514 $sSQL .= " SELECT max(p.importance*(p.rank_address+2))";
516 $sSQL .= " place_addressline s, ";
517 $sSQL .= " placex p";
518 $sSQL .= " WHERE s.place_id = min(blub.parent_place_id)";
519 $sSQL .= " AND p.place_id = s.address_place_id ";
520 $sSQL .= " AND s.isaddress";
521 $sSQL .= " AND p.importance is not null";
522 $sSQL .= " ) AS addressimportance, ";
523 $sSQL .= " null AS extra_place ";
525 $sSQL .= " SELECT place_id, "; // interpolate the Tiger housenumbers here
526 $sSQL .= " ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) AS centroid, ";
527 $sSQL .= " parent_place_id, ";
528 $sSQL .= " housenumber_for_place";
530 $sSQL .= " location_property_tiger ";
531 $sSQL .= " JOIN (values ".$sHousenumbers.") AS housenumbers(place_id, housenumber_for_place) USING(place_id)) ";
533 $sSQL .= " housenumber_for_place>=0";
534 $sSQL .= " AND 30 between $this->iMinAddressRank AND $this->iMaxAddressRank";
535 $sSQL .= " ) AS blub"; //postgres wants an alias here
536 $sSQL .= " GROUP BY";
537 $sSQL .= " place_id, ";
538 $sSQL .= " housenumber_for_place"; //is this group by really needed?, place_id + housenumber (in combination) are unique
539 if (!$this->bDeDupe) $sSQL .= ", place_id ";
542 // interpolation line search only if a housenumber was searched and if it was found (i.e. aPlaceIDs[placeID] = housenumber != -1) (realized through a join)
545 $sSQL .= " 'W' AS osm_type, ";
546 $sSQL .= " osm_id, ";
547 $sSQL .= " 'place' AS class, ";
548 $sSQL .= " 'house' AS type, ";
549 $sSQL .= " null AS admin_level, ";
550 $sSQL .= " 30 AS rank_search, ";
551 $sSQL .= " 30 AS rank_address, ";
552 $sSQL .= " min(place_id) as place_id, ";
553 $sSQL .= " min(parent_place_id) AS parent_place_id, ";
554 $sSQL .= " country_code, ";
555 $sSQL .= " get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) AS langaddress, ";
556 $sSQL .= " null AS placename, ";
557 $sSQL .= " null AS ref, ";
558 if ($this->bIncludeExtraTags) $sSQL .= "null AS extra, ";
559 if ($this->bIncludeNameDetails) $sSQL .= "null AS names, ";
560 $sSQL .= " AVG(st_x(centroid)) AS lon, ";
561 $sSQL .= " AVG(st_y(centroid)) AS lat, ";
562 $sSQL .= " ".$sImportanceSQL."-0.1 AS importance, "; // slightly smaller than the importance for normal houses with rank 30, which is 0
565 $sSQL .= " MAX(p.importance*(p.rank_address+2)) ";
567 $sSQL .= " place_addressline s, ";
568 $sSQL .= " placex p";
569 $sSQL .= " WHERE s.place_id = min(blub.parent_place_id) ";
570 $sSQL .= " AND p.place_id = s.address_place_id ";
571 $sSQL .= " AND s.isaddress ";
572 $sSQL .= " AND p.importance is not null";
573 $sSQL .= " ) AS addressimportance,";
574 $sSQL .= " null AS extra_place ";
577 $sSQL .= " osm_id, ";
578 $sSQL .= " place_id, ";
579 $sSQL .= " country_code, ";
580 $sSQL .= " CASE "; // interpolate the housenumbers here
581 $sSQL .= " WHEN startnumber != endnumber ";
582 $sSQL .= " THEN ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) ";
583 $sSQL .= " ELSE ST_LineInterpolatePoint(linegeo, 0.5) ";
584 $sSQL .= " END as centroid, ";
585 $sSQL .= " parent_place_id, ";
586 $sSQL .= " housenumber_for_place ";
588 $sSQL .= " location_property_osmline ";
589 $sSQL .= " JOIN (values ".$sHousenumbers.") AS housenumbers(place_id, housenumber_for_place) USING(place_id)";
591 $sSQL .= " WHERE housenumber_for_place>=0 ";
592 $sSQL .= " AND 30 between $this->iMinAddressRank AND $this->iMaxAddressRank";
593 $sSQL .= " ) as blub"; //postgres wants an alias here
594 $sSQL .= " GROUP BY ";
595 $sSQL .= " osm_id, ";
596 $sSQL .= " place_id, ";
597 $sSQL .= " housenumber_for_place, ";
598 $sSQL .= " country_code "; //is this group by really needed?, place_id + housenumber (in combination) are unique
599 if (!$this->bDeDupe) $sSQL .= ", place_id ";
601 if (CONST_Use_Aux_Location_data) {
604 $sSQL .= " 'L' AS osm_type, ";
605 $sSQL .= " place_id AS osm_id, ";
606 $sSQL .= " 'place' AS class,";
607 $sSQL .= " 'house' AS type, ";
608 $sSQL .= " null AS admin_level, ";
609 $sSQL .= " 0 AS rank_search,";
610 $sSQL .= " 0 AS rank_address, ";
611 $sSQL .= " min(place_id) AS place_id,";
612 $sSQL .= " min(parent_place_id) AS parent_place_id, ";
613 $sSQL .= " 'us' AS country_code, ";
614 $sSQL .= " get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) AS langaddress, ";
615 $sSQL .= " null AS placename, ";
616 $sSQL .= " null AS ref, ";
617 if ($this->bIncludeExtraTags) $sSQL .= "null AS extra, ";
618 if ($this->bIncludeNameDetails) $sSQL .= "null AS names, ";
619 $sSQL .= " avg(ST_X(centroid)) AS lon, ";
620 $sSQL .= " avg(ST_Y(centroid)) AS lat, ";
621 $sSQL .= " ".$sImportanceSQL."-1.10 AS importance, ";
623 $sSQL .= " SELECT max(p.importance*(p.rank_address+2))";
625 $sSQL .= " place_addressline s, ";
626 $sSQL .= " placex p";
627 $sSQL .= " WHERE s.place_id = min(location_property_aux.parent_place_id)";
628 $sSQL .= " AND p.place_id = s.address_place_id ";
629 $sSQL .= " AND s.isaddress";
630 $sSQL .= " AND p.importance is not null";
631 $sSQL .= " ) AS addressimportance, ";
632 $sSQL .= " null AS extra_place ";
633 $sSQL .= " FROM location_property_aux ";
634 $sSQL .= " WHERE place_id in ($sPlaceIDs) ";
635 $sSQL .= " AND 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
636 $sSQL .= " GROUP BY ";
637 $sSQL .= " place_id, ";
638 if (!$this->bDeDupe) $sSQL .= "place_id, ";
639 $sSQL .= " get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) ";
643 $sSQL .= " order by importance desc";
648 $aSearchResults = chksql(
649 $this->oDB->getAll($sSQL),
650 "Could not get details for place."
653 return $aSearchResults;
656 public function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases, $sNormQuery)
659 Calculate all searches using aValidTokens i.e.
660 'Wodsworth Road, Sheffield' =>
664 0 1 (wodsworth)(road)
667 Score how good the search is so they can be ordered
669 foreach ($aPhrases as $iPhrase => $aPhrase) {
670 $aNewPhraseSearches = array();
671 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
672 else $sPhraseType = '';
674 foreach ($aPhrase['wordsets'] as $iWordSet => $aWordset) {
675 // Too many permutations - too expensive
676 if ($iWordSet > 120) break;
678 $aWordsetSearches = $aSearches;
680 // Add all words from this wordset
681 foreach ($aWordset as $iToken => $sToken) {
682 //echo "<br><b>$sToken</b>";
683 $aNewWordsetSearches = array();
685 foreach ($aWordsetSearches as $aCurrentSearch) {
687 //var_dump($aCurrentSearch);
690 // If the token is valid
691 if (isset($aValidTokens[' '.$sToken])) {
692 foreach ($aValidTokens[' '.$sToken] as $aSearchTerm) {
693 $aSearch = $aCurrentSearch;
694 $aSearch['iSearchRank']++;
695 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0') {
696 if ($aSearch['sCountryCode'] === false) {
697 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
698 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
699 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases))) {
700 $aSearch['iSearchRank'] += 5;
702 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
704 } elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null) {
705 if ($aSearch['oNear'] === false) {
706 $aSearch['oNear'] = new NearPoint(
709 $aSearchTerm['radius']
711 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
713 } elseif ($sPhraseType == 'postalcode' || ($aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode')) {
714 // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
715 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
716 // If we have structured search or this is the first term,
717 // make the postcode the primary search element.
718 if ($sPhraseType == 'postalcode' || sizeof($aSearch['aName']) == 0) {
719 $aNewSearch = $aSearch;
720 $aNewSearch['sOperator'] = 'postcode';
721 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
722 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_token'];
723 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
726 // If we have a structured search or this is not the first term,
727 // add the postcode as an addendum.
728 if ($sPhraseType == 'postalcode' || sizeof($aSearch['aName'])) {
729 $aSearch['sPostcode'] = $aSearchTerm['word_token'];
730 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
733 } elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house') {
734 if ($aSearch['sHouseNumber'] === '') {
735 $aSearch['sHouseNumber'] = $sToken;
736 // sanity check: if the housenumber is not mainly made
737 // up of numbers, add a penalty
738 if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
739 // also housenumbers should appear in the first or second phrase
740 if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
741 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
743 // Fall back to not searching for this item (better than nothing)
744 $aSearch = $aCurrentSearch;
745 $aSearch['iSearchRank'] += 1;
746 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
749 } elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null) {
750 // require a normalized exact match of the term
751 // if we have the normalizer version of the query
753 if ($aSearch['sClass'] === ''
754 && ($sNormQuery === null || !($aSearchTerm['word'] && strpos($sNormQuery, $aSearchTerm['word']) === false))) {
755 $aSearch['sClass'] = $aSearchTerm['class'];
756 $aSearch['sType'] = $aSearchTerm['type'];
757 if ($aSearchTerm['operator'] == '') {
758 $aSearch['sOperator'] = sizeof($aSearch['aName']) ? 'name' : 'near';
759 $aSearch['iSearchRank'] += 2;
761 $aSearch['sOperator'] = 'near'; // near = in for the moment
764 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
766 } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
767 if (sizeof($aSearch['aName'])) {
768 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) {
769 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
771 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
772 $aSearch['iSearchRank'] += 1000; // skip;
775 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
776 //$aSearch['iNamePhrase'] = $iPhrase;
778 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
782 // Look for partial matches.
783 // Note that there is no point in adding country terms here
784 // because country are omitted in the address.
785 if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country') {
786 // Allow searching for a word - but at extra cost
787 foreach ($aValidTokens[$sToken] as $aSearchTerm) {
788 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
789 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false) {
790 $aSearch = $aCurrentSearch;
791 $aSearch['iSearchRank'] += 1;
792 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
793 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
794 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
795 } elseif (isset($aValidTokens[' '.$sToken])) { // revert to the token version?
796 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
797 $aSearch['iSearchRank'] += 1;
798 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
799 foreach ($aValidTokens[' '.$sToken] as $aSearchTermToken) {
800 if (empty($aSearchTermToken['country_code'])
801 && empty($aSearchTermToken['lat'])
802 && empty($aSearchTermToken['class'])
804 $aSearch = $aCurrentSearch;
805 $aSearch['iSearchRank'] += 1;
806 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
807 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
811 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
812 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
813 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
817 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase) {
818 $aSearch = $aCurrentSearch;
819 $aSearch['iSearchRank'] += 1;
820 if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
821 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
822 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
823 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
825 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
827 $aSearch['iNamePhrase'] = $iPhrase;
828 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
833 // Allow skipping a word - but at EXTREAM cost
834 //$aSearch = $aCurrentSearch;
835 //$aSearch['iSearchRank']+=100;
836 //$aNewWordsetSearches[] = $aSearch;
840 usort($aNewWordsetSearches, 'bySearchRank');
841 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
843 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
845 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
846 usort($aNewPhraseSearches, 'bySearchRank');
848 $aSearchHash = array();
849 foreach ($aNewPhraseSearches as $iSearch => $aSearch) {
850 $sHash = serialize($aSearch);
851 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
852 else $aSearchHash[$sHash] = 1;
855 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
858 // Re-group the searches by their score, junk anything over 20 as just not worth trying
859 $aGroupedSearches = array();
860 foreach ($aNewPhraseSearches as $aSearch) {
861 if ($aSearch['iSearchRank'] < $this->iMaxRank) {
862 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
863 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
866 ksort($aGroupedSearches);
869 $aSearches = array();
870 foreach ($aGroupedSearches as $iScore => $aNewSearches) {
871 $iSearchCount += sizeof($aNewSearches);
872 $aSearches = array_merge($aSearches, $aNewSearches);
873 if ($iSearchCount > 50) break;
876 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
878 return $aGroupedSearches;
881 /* Perform the actual query lookup.
883 Returns an ordered list of results, each with the following fields:
884 osm_type: type of corresponding OSM object
888 P - postcode (internally computed)
889 osm_id: id of corresponding OSM object
890 class: general object class (corresponds to tag key of primary OSM tag)
891 type: subclass of object (corresponds to tag value of primary OSM tag)
892 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
893 rank_search: rank in search hierarchy
894 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
895 rank_address: rank in address hierarchy (determines orer in address)
896 place_id: internal key (may differ between different instances)
897 country_code: ISO country code
898 langaddress: localized full address
899 placename: localized name of object
900 ref: content of ref tag (if available)
903 importance: importance of place based on Wikipedia link count
904 addressimportance: cumulated importance of address elements
905 extra_place: type of place (for admin boundaries, if there is a place tag)
906 aBoundingBox: bounding Box
907 label: short description of the object class/type (English only)
908 name: full name (currently the same as langaddress)
909 foundorder: secondary ordering for places with same importance
913 public function lookup()
915 if (!$this->sQuery && !$this->aStructuredQuery) return array();
917 $oNormalizer = \Transliterator::createFromRules(CONST_Term_Normalization_Rules);
918 if ($oNormalizer !== null) {
919 $sNormQuery = $oNormalizer->transliterate($this->sQuery);
924 $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
925 $sCountryCodesSQL = false;
926 if ($this->aCountryCodes) {
927 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
930 $sQuery = $this->sQuery;
931 if (!preg_match('//u', $sQuery)) {
932 userError("Query string is not UTF-8 encoded.");
935 // Conflicts between US state abreviations and various words for 'the' in different languages
936 if (isset($this->aLangPrefOrder['name:en'])) {
937 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/', '\1illinois\2', $sQuery);
938 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/', '\1alabama\2', $sQuery);
939 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/', '\1louisiana\2', $sQuery);
942 $bBoundingBoxSearch = $this->bBoundedSearch && $this->sViewboxSmallSQL;
943 if ($this->sViewboxCentreSQL) {
944 // For complex viewboxes (routes) precompute the bounding geometry
946 $this->oDB->getOne("select ".$this->sViewboxSmallSQL),
947 "Could not get small viewbox"
949 $this->sViewboxSmallSQL = "'".$sGeom."'::geometry";
952 $this->oDB->getOne("select ".$this->sViewboxLargeSQL),
953 "Could not get large viewbox"
955 $this->sViewboxLargeSQL = "'".$sGeom."'::geometry";
958 // Do we have anything that looks like a lat/lon pair?
960 if ($aLooksLike = NearPoint::extractFromQuery($sQuery)) {
961 $oNearPoint = $aLooksLike['pt'];
962 $sQuery = $aLooksLike['query'];
965 $aSearchResults = array();
966 if ($sQuery || $this->aStructuredQuery) {
967 // Start with a blank search
972 'sCountryCode' => false,
974 'aAddress' => array(),
975 'aFullNameAddress' => array(),
976 'aNameNonSearch' => array(),
977 'aAddressNonSearch' => array(),
979 'aFeatureName' => array(),
982 'sHouseNumber' => '',
984 'oNear' => $oNearPoint
988 // Any 'special' terms in the search?
989 $bSpecialTerms = false;
990 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
991 $aSpecialTerms = array();
992 foreach ($aSpecialTermsRaw as $aSpecialTerm) {
993 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
994 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
997 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
998 $aSpecialTerms = array();
999 if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity']) {
1000 $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
1001 unset($this->aStructuredQuery['amenity']);
1004 foreach ($aSpecialTermsRaw as $aSpecialTerm) {
1005 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
1006 $sToken = chksql($this->oDB->getOne("SELECT make_standard_name('".$aSpecialTerm[1]."') AS string"));
1007 $sSQL = 'SELECT * ';
1009 $sSQL .= ' SELECT word_id, word_token, word, class, type, country_code, operator';
1010 $sSQL .= ' FROM word ';
1011 $sSQL .= ' WHERE word_token in (\' '.$sToken.'\')';
1013 $sSQL .= ' WHERE (class is not null AND class not in (\'place\')) ';
1014 $sSQL .= ' OR country_code is not null';
1015 if (CONST_Debug) var_Dump($sSQL);
1016 $aSearchWords = chksql($this->oDB->getAll($sSQL));
1017 $aNewSearches = array();
1018 foreach ($aSearches as $aSearch) {
1019 foreach ($aSearchWords as $aSearchTerm) {
1020 $aNewSearch = $aSearch;
1021 if ($aSearchTerm['country_code']) {
1022 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
1023 $aNewSearches[] = $aNewSearch;
1024 $bSpecialTerms = true;
1026 if ($aSearchTerm['class']) {
1027 $aNewSearch['sClass'] = $aSearchTerm['class'];
1028 $aNewSearch['sType'] = $aSearchTerm['type'];
1029 $aNewSearches[] = $aNewSearch;
1030 $bSpecialTerms = true;
1034 $aSearches = $aNewSearches;
1037 // Split query into phrases
1038 // Commas are used to reduce the search space by indicating where phrases split
1039 if ($this->aStructuredQuery) {
1040 $aPhrases = $this->aStructuredQuery;
1041 $bStructuredPhrases = true;
1043 $aPhrases = explode(',', $sQuery);
1044 $bStructuredPhrases = false;
1047 // Convert each phrase to standard form
1048 // Create a list of standard words
1049 // Get all 'sets' of words
1050 // Generate a complete list of all
1052 foreach ($aPhrases as $iPhrase => $sPhrase) {
1054 $this->oDB->getRow("SELECT make_standard_name('".pg_escape_string($sPhrase)."') as string"),
1055 "Cannot normalize query string (is it a UTF-8 string?)"
1057 if (trim($aPhrase['string'])) {
1058 $aPhrases[$iPhrase] = $aPhrase;
1059 $aPhrases[$iPhrase]['words'] = explode(' ', $aPhrases[$iPhrase]['string']);
1060 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
1061 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
1063 unset($aPhrases[$iPhrase]);
1067 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
1068 $aPhraseTypes = array_keys($aPhrases);
1069 $aPhrases = array_values($aPhrases);
1071 if (sizeof($aTokens)) {
1072 // Check which tokens we have, get the ID numbers
1073 $sSQL = 'SELECT word_id, word_token, word, class, type, country_code, operator, search_name_count';
1074 $sSQL .= ' FROM word ';
1075 $sSQL .= ' WHERE word_token in ('.join(',', array_map("getDBQuoted", $aTokens)).')';
1077 if (CONST_Debug) var_Dump($sSQL);
1079 $aValidTokens = array();
1080 if (sizeof($aTokens)) {
1081 $aDatabaseWords = chksql(
1082 $this->oDB->getAll($sSQL),
1083 "Could not get word tokens."
1086 $aDatabaseWords = array();
1088 $aPossibleMainWordIDs = array();
1089 $aWordFrequencyScores = array();
1090 foreach ($aDatabaseWords as $aToken) {
1091 // Very special case - require 2 letter country param to match the country code found
1092 if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
1093 && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code']
1098 if (isset($aValidTokens[$aToken['word_token']])) {
1099 $aValidTokens[$aToken['word_token']][] = $aToken;
1101 $aValidTokens[$aToken['word_token']] = array($aToken);
1103 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
1104 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
1106 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
1108 // Try and calculate GB postcodes we might be missing
1109 foreach ($aTokens as $sToken) {
1110 // Source of gb postcodes is now definitive - always use
1111 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData)) {
1112 if (substr($aData[1], -2, 1) != ' ') {
1113 $aData[0] = substr($aData[0], 0, strlen($aData[1])-1).' '.substr($aData[0], strlen($aData[1])-1);
1114 $aData[1] = substr($aData[1], 0, -1).' '.substr($aData[1], -1, 1);
1116 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
1117 if ($aGBPostcodeLocation) {
1118 $aValidTokens[$sToken] = $aGBPostcodeLocation;
1120 } elseif (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData)) {
1121 // US ZIP+4 codes - if there is no token,
1122 // merge in the 5-digit ZIP code
1123 if (isset($aValidTokens[$aData[1]])) {
1124 foreach ($aValidTokens[$aData[1]] as $aToken) {
1125 if (!$aToken['class']) {
1126 if (isset($aValidTokens[$sToken])) {
1127 $aValidTokens[$sToken][] = $aToken;
1129 $aValidTokens[$sToken] = array($aToken);
1137 foreach ($aTokens as $sToken) {
1138 // Unknown single word token with a number - assume it is a house number
1139 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken, ' ') === false && preg_match('/[0-9]/', $sToken)) {
1140 $aValidTokens[' '.$sToken] = array(array('class' => 'place', 'type' => 'house'));
1144 // Any words that have failed completely?
1145 // TODO: suggestions
1147 // Start the search process
1148 // array with: placeid => -1 | tiger-housenumber
1149 $aResultPlaceIDs = array();
1151 $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases, $sNormQuery);
1153 if ($this->bReverseInPlan) {
1154 // Reverse phrase array and also reverse the order of the wordsets in
1155 // the first and final phrase. Don't bother about phrases in the middle
1156 // because order in the address doesn't matter.
1157 $aPhrases = array_reverse($aPhrases);
1158 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1159 if (sizeof($aPhrases) > 1) {
1160 $aFinalPhrase = end($aPhrases);
1161 $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1163 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false, $sNormQuery);
1165 foreach ($aGroupedSearches as $aSearches) {
1166 foreach ($aSearches as $aSearch) {
1167 if ($aSearch['iSearchRank'] < $this->iMaxRank) {
1168 if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1169 $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1174 $aGroupedSearches = $aReverseGroupedSearches;
1175 ksort($aGroupedSearches);
1178 // Re-group the searches by their score, junk anything over 20 as just not worth trying
1179 $aGroupedSearches = array();
1180 foreach ($aSearches as $aSearch) {
1181 if ($aSearch['iSearchRank'] < $this->iMaxRank) {
1182 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1183 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1186 ksort($aGroupedSearches);
1189 if (CONST_Debug) var_Dump($aGroupedSearches);
1190 if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0) {
1191 $aCopyGroupedSearches = $aGroupedSearches;
1192 foreach ($aCopyGroupedSearches as $iGroup => $aSearches) {
1193 foreach ($aSearches as $iSearch => $aSearch) {
1194 $aReductionsList = array($aSearch['aAddress']);
1195 $iSearchRank = $aSearch['iSearchRank'];
1196 while (sizeof($aReductionsList) > 0) {
1198 if ($iSearchRank > iMaxRank) break 3;
1199 $aNewReductionsList = array();
1200 foreach ($aReductionsList as $aReductionsWordList) {
1201 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++) {
1202 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1203 $aReverseSearch = $aSearch;
1204 $aSearch['aAddress'] = $aReductionsWordListResult;
1205 $aSearch['iSearchRank'] = $iSearchRank;
1206 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1207 if (sizeof($aReductionsWordListResult) > 0) {
1208 $aNewReductionsList[] = $aReductionsWordListResult;
1212 $aReductionsList = $aNewReductionsList;
1216 ksort($aGroupedSearches);
1219 // Filter out duplicate searches
1220 $aSearchHash = array();
1221 foreach ($aGroupedSearches as $iGroup => $aSearches) {
1222 foreach ($aSearches as $iSearch => $aSearch) {
1223 $sHash = serialize($aSearch);
1224 if (isset($aSearchHash[$sHash])) {
1225 unset($aGroupedSearches[$iGroup][$iSearch]);
1226 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1228 $aSearchHash[$sHash] = 1;
1233 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1237 foreach ($aGroupedSearches as $iGroupedRank => $aSearches) {
1239 foreach ($aSearches as $aSearch) {
1241 $searchedHousenumber = -1;
1243 if (CONST_Debug) echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>";
1244 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1246 // No location term?
1247 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['oNear']) {
1248 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber']) {
1249 // Just looking for a country by code - look it up
1250 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank) {
1251 $sSQL = "SELECT place_id FROM placex WHERE country_code='".$aSearch['sCountryCode']."' AND rank_search = 4";
1252 if ($sCountryCodesSQL) $sSQL .= " AND country_code in ($sCountryCodesSQL)";
1253 if ($bBoundingBoxSearch)
1254 $sSQL .= " AND _st_intersects($this->sViewboxSmallSQL, geometry)";
1255 $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
1256 if (CONST_Debug) var_dump($sSQL);
1257 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1259 $aPlaceIDs = array();
1262 if (!$bBoundingBoxSearch && !$aSearch['oNear']) continue;
1263 if (!$aSearch['sClass']) continue;
1265 $sSQL = "SELECT COUNT(*) FROM pg_tables WHERE tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1266 if (chksql($this->oDB->getOne($sSQL))) {
1267 $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1268 if ($sCountryCodesSQL) $sSQL .= " JOIN placex USING (place_id)";
1269 $sSQL .= " WHERE st_contains($this->sViewboxSmallSQL, ct.centroid)";
1270 if ($sCountryCodesSQL) $sSQL .= " AND country_code in ($sCountryCodesSQL)";
1271 if (sizeof($this->aExcludePlaceIDs)) {
1272 $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1274 if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC";
1275 $sSQL .= " limit $this->iLimit";
1276 if (CONST_Debug) var_dump($sSQL);
1277 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1279 // If excluded place IDs are given, it is fair to assume that
1280 // there have been results in the small box, so no further
1281 // expansion in that case.
1282 // Also don't expand if bounded results were requested.
1283 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch) {
1284 $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1285 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1286 $sSQL .= " WHERE ST_Contains($this->sViewboxLargeSQL, ct.centroid)";
1287 if ($sCountryCodesSQL) $sSQL .= " AND country_code in ($sCountryCodesSQL)";
1288 if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC";
1289 $sSQL .= " LIMIT $this->iLimit";
1290 if (CONST_Debug) var_dump($sSQL);
1291 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1294 $sSQL = "SELECT place_id ";
1295 $sSQL .= "FROM placex ";
1296 $sSQL .= "WHERE class='".$aSearch['sClass']."' ";
1297 $sSQL .= " AND type='".$aSearch['sType']."'";
1298 $sSQL .= " AND ST_Contains($this->sViewboxSmallSQL, geometry) ";
1299 $sSQL .= " AND linked_place_id is null";
1300 if ($sCountryCodesSQL) $sSQL .= " AND country_code in ($sCountryCodesSQL)";
1301 if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, centroid) ASC";
1302 $sSQL .= " LIMIT $this->iLimit";
1303 if (CONST_Debug) var_dump($sSQL);
1304 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1307 } elseif ($aSearch['oNear'] && !sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['sClass']) {
1308 // If a coordinate is given, the search must either
1309 // be for a name or a special search. Ignore everythin else.
1310 $aPlaceIDs = array();
1312 $aPlaceIDs = array();
1314 // First we need a position, either aName or fLat or both
1318 if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress'])) {
1319 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1322 $aOrder[0] .= " EXISTS(";
1323 $aOrder[0] .= " SELECT place_id ";
1324 $aOrder[0] .= " FROM placex ";
1325 $aOrder[0] .= " WHERE parent_place_id = search_name.place_id";
1326 $aOrder[0] .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."' ";
1327 $aOrder[0] .= " LIMIT 1";
1328 $aOrder[0] .= " ) ";
1329 // also housenumbers from interpolation lines table are needed
1330 $aOrder[0] .= " OR EXISTS(";
1331 $aOrder[0] .= " SELECT place_id ";
1332 $aOrder[0] .= " FROM location_property_osmline ";
1333 $aOrder[0] .= " WHERE parent_place_id = search_name.place_id";
1334 $aOrder[0] .= " AND startnumber is not NULL";
1335 $aOrder[0] .= " AND ".intval($aSearch['sHouseNumber']).">=startnumber ";
1336 $aOrder[0] .= " AND ".intval($aSearch['sHouseNumber'])."<=endnumber ";
1337 $aOrder[0] .= " LIMIT 1";
1340 $aOrder[0] .= " DESC";
1343 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1344 // they might be right - but they are just too darned expensive to run
1345 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'], ",")."]";
1346 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'], ",")."]";
1347 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress']) {
1348 // For infrequent name terms disable index usage for address
1349 if (CONST_Search_NameOnlySearchFrequencyThreshold
1350 && sizeof($aSearch['aName']) == 1
1351 && $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold
1353 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'], $aSearch['aAddressNonSearch']), ",")."]";
1355 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'], ",")."]";
1356 if (sizeof($aSearch['aAddressNonSearch'])) {
1357 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'], ",")."]";
1361 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1362 if ($aSearch['sHouseNumber']) {
1363 $aTerms[] = "address_rank between 16 and 27";
1365 if ($this->iMinAddressRank > 0) {
1366 $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1368 if ($this->iMaxAddressRank < 30) {
1369 $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1372 if ($aSearch['oNear']) {
1373 $aTerms[] = $aSearch['oNear']->withinSQL('centroid');
1375 $aOrder[] = $aSearch['oNear']->distanceSQL('centroid');
1377 if (sizeof($this->aExcludePlaceIDs)) {
1378 $aTerms[] = "place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1380 if ($sCountryCodesSQL) {
1381 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1384 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1386 $aOrder[] = $oNearPoint->distanceSQL('centroid');
1389 if ($aSearch['sHouseNumber']) {
1390 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1392 $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
1394 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1395 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1397 $aOrder[] = "$sImportanceSQL DESC";
1398 if (sizeof($aSearch['aFullNameAddress'])) {
1399 $sExactMatchSQL = ' ( ';
1400 $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
1401 $sExactMatchSQL .= ' SELECT unnest(ARRAY['.join($aSearch['aFullNameAddress'], ",").']) ';
1402 $sExactMatchSQL .= ' INTERSECT ';
1403 $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)';
1404 $sExactMatchSQL .= ' ) s';
1405 $sExactMatchSQL .= ') as exactmatch';
1406 $aOrder[] = 'exactmatch DESC';
1408 $sExactMatchSQL = '0::int as exactmatch';
1411 if (sizeof($aTerms)) {
1412 $sSQL = "SELECT place_id, ";
1413 $sSQL .= $sExactMatchSQL;
1414 $sSQL .= " FROM search_name";
1415 $sSQL .= " WHERE ".join(' and ', $aTerms);
1416 $sSQL .= " ORDER BY ".join(', ', $aOrder);
1417 if ($aSearch['sHouseNumber'] || $aSearch['sClass']) {
1418 $sSQL .= " LIMIT 20";
1419 } elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass']) {
1420 $sSQL .= " LIMIT 1";
1422 $sSQL .= " LIMIT ".$this->iLimit;
1425 if (CONST_Debug) var_dump($sSQL);
1426 $aViewBoxPlaceIDs = chksql(
1427 $this->oDB->getAll($sSQL),
1428 "Could not get places for search terms."
1430 //var_dump($aViewBoxPlaceIDs);
1431 // Did we have an viewbox matches?
1432 $aPlaceIDs = array();
1433 $bViewBoxMatch = false;
1434 foreach ($aViewBoxPlaceIDs as $aViewBoxRow) {
1435 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1436 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1437 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1438 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1439 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1440 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1443 //var_Dump($aPlaceIDs);
1446 //now search for housenumber, if housenumber provided
1447 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs)) {
1448 $searchedHousenumber = intval($aSearch['sHouseNumber']);
1449 $aRoadPlaceIDs = $aPlaceIDs;
1450 $sPlaceIDs = join(',', $aPlaceIDs);
1452 // Now they are indexed, look for a house attached to a street we found
1453 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1454 $sSQL = "SELECT place_id FROM placex ";
1455 $sSQL .= "WHERE parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1456 if (sizeof($this->aExcludePlaceIDs)) {
1457 $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1459 $sSQL .= " LIMIT $this->iLimit";
1460 if (CONST_Debug) var_dump($sSQL);
1461 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1463 // if nothing found, search in the interpolation line table
1464 if (!sizeof($aPlaceIDs)) {
1465 // do we need to use transliteration and the regex for housenumbers???
1466 //new query for lines, not housenumbers anymore
1467 $sSQL = "SELECT distinct place_id FROM location_property_osmline";
1468 $sSQL .= " WHERE startnumber is not NULL and parent_place_id in (".$sPlaceIDs.") and (";
1469 if ($searchedHousenumber%2 == 0) {
1470 //if housenumber is even, look for housenumber in streets with interpolationtype even or all
1471 $sSQL .= "interpolationtype='even'";
1473 //look for housenumber in streets with interpolationtype odd or all
1474 $sSQL .= "interpolationtype='odd'";
1476 $sSQL .= " or interpolationtype='all') and ";
1477 $sSQL .= $searchedHousenumber.">=startnumber and ";
1478 $sSQL .= $searchedHousenumber."<=endnumber";
1480 if (sizeof($this->aExcludePlaceIDs)) {
1481 $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1483 //$sSQL .= " limit $this->iLimit";
1484 if (CONST_Debug) var_dump($sSQL);
1486 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
1489 // If nothing found try the aux fallback table
1490 if (CONST_Use_Aux_Location_data && !sizeof($aPlaceIDs)) {
1491 $sSQL = "SELECT place_id FROM location_property_aux ";
1492 $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") ";
1493 $sSQL .= " AND housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1494 if (sizeof($this->aExcludePlaceIDs)) {
1495 $sSQL .= " AND parent_place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1497 //$sSQL .= " limit $this->iLimit";
1498 if (CONST_Debug) var_dump($sSQL);
1499 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1502 //if nothing was found in placex or location_property_aux, then search in Tiger data for this housenumber(location_property_tiger)
1503 if (CONST_Use_US_Tiger_Data && !sizeof($aPlaceIDs)) {
1504 $sSQL = "SELECT distinct place_id FROM location_property_tiger";
1505 $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") and (";
1506 if ($searchedHousenumber%2 == 0) {
1507 $sSQL .= "interpolationtype='even'";
1509 $sSQL .= "interpolationtype='odd'";
1511 $sSQL .= " or interpolationtype='all') and ";
1512 $sSQL .= $searchedHousenumber.">=startnumber and ";
1513 $sSQL .= $searchedHousenumber."<=endnumber";
1515 if (sizeof($this->aExcludePlaceIDs)) {
1516 $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1518 //$sSQL .= " limit $this->iLimit";
1519 if (CONST_Debug) var_dump($sSQL);
1521 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
1524 // Fallback to the road (if no housenumber was found)
1525 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber'])) {
1526 $aPlaceIDs = $aRoadPlaceIDs;
1527 //set to -1, if no housenumbers were found
1528 $searchedHousenumber = -1;
1530 //else: housenumber was found, remains saved in searchedHousenumber
1534 if ($aSearch['sClass'] && sizeof($aPlaceIDs)) {
1535 $sPlaceIDs = join(',', $aPlaceIDs);
1536 $aClassPlaceIDs = array();
1538 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name') {
1539 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1540 $sSQL = "SELECT place_id ";
1541 $sSQL .= " FROM placex ";
1542 $sSQL .= " WHERE place_id in ($sPlaceIDs) ";
1543 $sSQL .= " AND class='".$aSearch['sClass']."' ";
1544 $sSQL .= " AND type='".$aSearch['sType']."'";
1545 $sSQL .= " AND linked_place_id is null";
1546 if ($sCountryCodesSQL) $sSQL .= " AND country_code in ($sCountryCodesSQL)";
1547 $sSQL .= " ORDER BY rank_search ASC ";
1548 $sSQL .= " LIMIT $this->iLimit";
1549 if (CONST_Debug) var_dump($sSQL);
1550 $aClassPlaceIDs = chksql($this->oDB->getCol($sSQL));
1553 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') { // & in
1554 $sClassTable = 'place_classtype_'.$aSearch['sClass'].'_'.$aSearch['sType'];
1555 $sSQL = "SELECT count(*) FROM pg_tables ";
1556 $sSQL .= "WHERE tablename = '$sClassTable'";
1557 $bCacheTable = chksql($this->oDB->getOne($sSQL));
1559 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
1561 if (CONST_Debug) var_dump($sSQL);
1562 $this->iMaxRank = ((int)chksql($this->oDB->getOne($sSQL)));
1564 // For state / country level searches the normal radius search doesn't work very well
1565 $sPlaceGeom = false;
1566 if ($this->iMaxRank < 9 && $bCacheTable) {
1567 // Try and get a polygon to search in instead
1568 $sSQL = "SELECT geometry ";
1569 $sSQL .= " FROM placex";
1570 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
1571 $sSQL .= " AND rank_search < $this->iMaxRank + 5";
1572 $sSQL .= " AND ST_Geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon')";
1573 $sSQL .= " ORDER BY rank_search ASC ";
1574 $sSQL .= " LIMIT 1";
1575 if (CONST_Debug) var_dump($sSQL);
1576 $sPlaceGeom = chksql($this->oDB->getOne($sSQL));
1582 $this->iMaxRank += 5;
1583 $sSQL = "SELECT place_id FROM placex WHERE place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1584 if (CONST_Debug) var_dump($sSQL);
1585 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1586 $sPlaceIDs = join(',', $aPlaceIDs);
1589 if ($sPlaceIDs || $sPlaceGeom) {
1592 // More efficient - can make the range bigger
1597 $sOrderBySQL = $oNearPoint->distanceSQL('l.centroid');
1598 } elseif ($sPlaceIDs) {
1599 $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1600 } elseif ($sPlaceGeom) {
1601 $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1604 $sSQL = "select distinct i.place_id".($sOrderBySQL?', i.order_term':'')." from (";
1605 $sSQL .= "select l.place_id".($sOrderBySQL?','.$sOrderBySQL.' as order_term':'')." from ".$sClassTable." as l";
1606 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1608 $sSQL .= ",placex as f where ";
1609 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1613 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1615 if (sizeof($this->aExcludePlaceIDs)) {
1616 $sSQL .= " and l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1618 if ($sCountryCodesSQL) $sSQL .= " and lp.country_code in ($sCountryCodesSQL)";
1619 $sSQL .= 'limit 300) i ';
1620 if ($sOrderBySQL) $sSQL .= "order by order_term asc";
1621 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1622 $sSQL .= " limit $this->iLimit";
1623 if (CONST_Debug) var_dump($sSQL);
1624 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
1626 if ($aSearch['oNear']) {
1627 $fRange = $aSearch['oNear']->radius();
1632 $sOrderBySQL = $oNearPoint->distanceSQL('l.geometry');
1634 $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1637 $sSQL = "SELECT distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'');
1638 $sSQL .= " FROM placex as l, placex as f ";
1639 $sSQL .= " WHERE f.place_id in ($sPlaceIDs) ";
1640 $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange) ";
1641 $sSQL .= " AND l.class='".$aSearch['sClass']."' ";
1642 $sSQL .= " AND l.type='".$aSearch['sType']."' ";
1643 if (sizeof($this->aExcludePlaceIDs)) {
1644 $sSQL .= " AND l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1646 if ($sCountryCodesSQL) $sSQL .= " AND l.country_code in ($sCountryCodesSQL)";
1647 if ($sOrderBy) $sSQL .= "ORDER BY ".$OrderBysSQL." ASC";
1648 if ($this->iOffset) $sSQL .= " OFFSET $this->iOffset";
1649 $sSQL .= " limit $this->iLimit";
1650 if (CONST_Debug) var_dump($sSQL);
1651 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
1655 $aPlaceIDs = $aClassPlaceIDs;
1660 echo "<br><b>Place IDs:</b> ";
1661 var_Dump($aPlaceIDs);
1664 foreach ($aPlaceIDs as $iPlaceID) {
1665 // array for placeID => -1 | Tiger housenumber
1666 $aResultPlaceIDs[$iPlaceID] = $searchedHousenumber;
1668 if ($iQueryLoop > 20) break;
1671 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30)) {
1672 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1673 // reduces the number of place ids, like a filter
1674 // rank_address is 30 for interpolated housenumbers
1675 $sSQL = "SELECT place_id ";
1676 $sSQL .= "FROM placex ";
1677 $sSQL .= "WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).") ";
1679 $sSQL .= " placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1680 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) {
1681 $sSQL .= " OR (extratags->'place') = 'city'";
1683 if ($this->aAddressRankList) {
1684 $sSQL .= " OR placex.rank_address in (".join(',', $this->aAddressRankList).")";
1686 if (CONST_Use_US_Tiger_Data) {
1689 $sSQL .= " SELECT place_id ";
1690 $sSQL .= " FROM location_property_tiger ";
1691 $sSQL .= " WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).") ";
1692 $sSQL .= " AND (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1693 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',', $this->aAddressRankList).")";
1695 $sSQL .= ") UNION ";
1696 $sSQL .= " SELECT place_id ";
1697 $sSQL .= " FROM location_property_osmline ";
1698 $sSQL .= " WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).")";
1699 $sSQL .= " AND startnumber is not NULL AND (30 between $this->iMinAddressRank and $this->iMaxAddressRank)";
1700 if (CONST_Debug) var_dump($sSQL);
1701 $aFilteredPlaceIDs = chksql($this->oDB->getCol($sSQL));
1703 foreach ($aFilteredPlaceIDs as $placeID) {
1704 $tempIDs[$placeID] = $aResultPlaceIDs[$placeID]; //assign housenumber to placeID
1706 $aResultPlaceIDs = $tempIDs;
1710 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1711 if ($iGroupLoop > 4) break;
1712 if ($iQueryLoop > 30) break;
1715 // Did we find anything?
1716 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) {
1717 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1720 // Just interpret as a reverse geocode
1721 $oReverse = new ReverseGeocode($this->oDB);
1722 $oReverse->setZoom(18);
1724 $aLookup = $oReverse->lookup(
1730 if (CONST_Debug) var_dump("Reverse search", $aLookup);
1732 if ($aLookup['place_id']) {
1733 $aSearchResults = $this->getDetails(array($aLookup['place_id'] => -1));
1734 $aResultPlaceIDs[$aLookup['place_id']] = -1;
1736 $aSearchResults = array();
1741 if (!sizeof($aSearchResults)) {
1742 if ($this->bFallback) {
1743 if ($this->fallbackStructuredQuery()) {
1744 return $this->lookup();
1751 $aClassType = getClassTypesWithImportance();
1752 $aRecheckWords = preg_split('/\b[\s,\\-]*/u', $sQuery);
1753 foreach ($aRecheckWords as $i => $sWord) {
1754 if (!preg_match('/[\pL\pN]/', $sWord)) unset($aRecheckWords[$i]);
1758 echo '<i>Recheck words:<\i>';
1759 var_dump($aRecheckWords);
1762 $oPlaceLookup = new PlaceLookup($this->oDB);
1763 $oPlaceLookup->setIncludePolygonAsPoints($this->bIncludePolygonAsPoints);
1764 $oPlaceLookup->setIncludePolygonAsText($this->bIncludePolygonAsText);
1765 $oPlaceLookup->setIncludePolygonAsGeoJSON($this->bIncludePolygonAsGeoJSON);
1766 $oPlaceLookup->setIncludePolygonAsKML($this->bIncludePolygonAsKML);
1767 $oPlaceLookup->setIncludePolygonAsSVG($this->bIncludePolygonAsSVG);
1768 $oPlaceLookup->setPolygonSimplificationThreshold($this->fPolygonSimplificationThreshold);
1770 foreach ($aSearchResults as $iResNum => $aResult) {
1772 $fDiameter = getResultDiameter($aResult);
1774 $aOutlineResult = $oPlaceLookup->getOutlines($aResult['place_id'], $aResult['lon'], $aResult['lat'], $fDiameter/2);
1775 if ($aOutlineResult) {
1776 $aResult = array_merge($aResult, $aOutlineResult);
1779 if ($aResult['extra_place'] == 'city') {
1780 $aResult['class'] = 'place';
1781 $aResult['type'] = 'city';
1782 $aResult['rank_search'] = 16;
1785 // Is there an icon set for this type of result?
1786 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1787 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon']
1789 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1792 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1793 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label']
1795 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1796 } elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1797 && $aClassType[$aResult['class'].':'.$aResult['type']]['label']
1799 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1801 // if tag '&addressdetails=1' is set in query
1802 if ($this->bIncludeAddressDetails) {
1803 // getAddressDetails() is defined in lib.php and uses the SQL function get_addressdata in functions.sql
1804 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code'], $aResultPlaceIDs[$aResult['place_id']]);
1805 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city'])) {
1806 $aResult['address'] = array_merge(array('city' => array_values($aResult['address'])[0]), $aResult['address']);
1810 if ($this->bIncludeExtraTags) {
1811 if ($aResult['extra']) {
1812 $aResult['sExtraTags'] = json_decode($aResult['extra']);
1814 $aResult['sExtraTags'] = (object) array();
1818 if ($this->bIncludeNameDetails) {
1819 if ($aResult['names']) {
1820 $aResult['sNameDetails'] = json_decode($aResult['names']);
1822 $aResult['sNameDetails'] = (object) array();
1826 // Adjust importance for the number of exact string matches in the result
1827 $aResult['importance'] = max(0.001, $aResult['importance']);
1829 $sAddress = $aResult['langaddress'];
1830 foreach ($aRecheckWords as $i => $sWord) {
1831 if (stripos($sAddress, $sWord)!==false) {
1833 if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1837 $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
1839 $aResult['name'] = $aResult['langaddress'];
1840 // secondary ordering (for results with same importance (the smaller the better):
1841 // - approximate importance of address parts
1842 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1843 // - number of exact matches from the query
1844 if (isset($this->exactMatchCache[$aResult['place_id']])) {
1845 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1846 } elseif (isset($this->exactMatchCache[$aResult['parent_place_id']])) {
1847 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1849 // - importance of the class/type
1850 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1851 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance']
1853 $aResult['foundorder'] += 0.0001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1855 $aResult['foundorder'] += 0.01;
1857 if (CONST_Debug) var_dump($aResult);
1858 $aSearchResults[$iResNum] = $aResult;
1860 uasort($aSearchResults, 'byImportance');
1862 $aOSMIDDone = array();
1863 $aClassTypeNameDone = array();
1864 $aToFilter = $aSearchResults;
1865 $aSearchResults = array();
1868 foreach ($aToFilter as $iResNum => $aResult) {
1869 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1871 $fLat = $aResult['lat'];
1872 $fLon = $aResult['lon'];
1873 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1876 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1877 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']]))
1879 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1880 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1881 $aSearchResults[] = $aResult;
1884 // Absolute limit on number of results
1885 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1888 return $aSearchResults;