5 require_once(CONST_BasePath.'/lib/SpecialSearchOperator.php');
6 require_once(CONST_BasePath.'/lib/SearchContext.php');
9 * Description of a single interpretation of a search query.
11 class SearchDescription
13 /// Ranking how well the description fits the query.
14 private $iSearchRank = 0;
15 /// Country code of country the result must belong to.
16 private $sCountryCode = '';
17 /// List of word ids making up the name of the object.
18 private $aName = array();
19 /// List of word ids making up the address of the object.
20 private $aAddress = array();
21 /// Subset of word ids of full words making up the address.
22 private $aFullNameAddress = array();
23 /// List of word ids that appear in the name but should be ignored.
24 private $aNameNonSearch = array();
25 /// List of word ids that appear in the address but should be ignored.
26 private $aAddressNonSearch = array();
27 /// Kind of search for special searches, see Nominatim::Operator.
28 private $iOperator = Operator::NONE;
29 /// Class of special feature to search for.
31 /// Type of special feature to search for.
33 /// Housenumber of the object.
34 private $sHouseNumber = '';
35 /// Postcode for the object.
36 private $sPostcode = '';
37 /// Global search constraints.
40 // Temporary values used while creating the search description.
42 /// Index of phrase currently processed.
43 private $iNamePhrase = -1;
46 public function __construct($oContext)
48 $this->oContext = $oContext;
51 public function getRank()
53 return $this->iSearchRank;
56 public function addToRank($iAddRank)
58 $this->iSearchRank += $iAddRank;
59 return $this->iSearchRank;
62 public function setPoiSearch($iOperator, $sClass, $sType)
64 $this->iOperator = $iOperator;
65 $this->sClass = $sClass;
66 $this->sType = $sType;
69 public function looksLikeFullAddress()
71 return sizeof($this->aName)
72 && (sizeof($this->aAddress || $this->sCountryCode))
73 && preg_match('/[0-9]+/', $this->sHouseNumber);
76 public function hasOperator()
78 return $this->iOperator != Operator::NONE;
81 public function extractKeyValuePairs($sQuery)
83 // Search for terms of kind [<key>=<value>].
85 '/\\[([\\w_]*)=([\\w_]*)\\]/',
91 foreach ($aSpecialTermsRaw as $aTerm) {
92 $sQuery = str_replace($aTerm[0], ' ', $sQuery);
93 if (!$this->hasOperator()) {
94 $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
101 public function isValidSearch(&$aCountryCodes)
103 if (!sizeof($this->aName)) {
104 if ($this->sHouseNumber) {
109 && $this->sCountryCode
110 && !in_array($this->sCountryCode, $aCountryCodes)
118 /////////// Search building functions
121 public function extendWithFullTerm($aSearchTerm, $bWordInQuery, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken, &$iGlobalRank)
123 $aNewSearches = array();
125 if (($sPhraseType == '' || $sPhraseType == 'country')
126 && !empty($aSearchTerm['country_code'])
127 && $aSearchTerm['country_code'] != '0'
129 if (!$this->sCountryCode) {
130 $oSearch = clone $this;
131 $oSearch->iSearchRank++;
132 $oSearch->sCountryCode = $aSearchTerm['country_code'];
133 // Country is almost always at the end of the string
134 // - increase score for finding it anywhere else (optimisation)
136 $oSearch->iSearchRank += 5;
138 $aNewSearches[] = $oSearch;
140 // If it is at the beginning, we can be almost sure that
141 // the terms are in the wrong order. Increase score for all searches.
146 } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode')
147 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode'
149 // We need to try the case where the postal code is the primary element
150 // (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode)
152 if (!$this->sPostcode && $bWordInQuery
153 && pg_escape_string($aSearchTerm['word']) == $aSearchTerm['word']
155 // If we have structured search or this is the first term,
156 // make the postcode the primary search element.
157 if ($this->iOperator == Operator::NONE
158 && ($sPhraseType == 'postalcode' || $bFirstToken)
160 $oSearch = clone $this;
161 $oSearch->iSearchRank++;
162 $oSearch->iOperator = Operator::POSTCODE;
163 $oSearch->aAddress = array_merge($this->aAddress, $this->aName);
165 array($aSearchTerm['word_id'] => $aSearchTerm['word']);
166 $aNewSearches[] = $oSearch;
169 // If we have a structured search or this is not the first term,
170 // add the postcode as an addendum.
171 if ($this->iOperator != Operator::POSTCODE
172 && ($sPhraseType == 'postalcode' || sizeof($this->aName))
174 $oSearch = clone $this;
175 $oSearch->iSearchRank++;
176 $oSearch->sPostcode = $aSearchTerm['word'];
177 $aNewSearches[] = $oSearch;
180 } elseif (($sPhraseType == '' || $sPhraseType == 'street')
181 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house'
183 if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
184 $oSearch = clone $this;
185 $oSearch->iSearchRank++;
186 $oSearch->sHouseNumber = trim($aSearchTerm['word_token']);
187 // sanity check: if the housenumber is not mainly made
188 // up of numbers, add a penalty
189 if (preg_match_all("/[^0-9]/", $oSearch->sHouseNumber, $aMatches) > 2) {
190 $oSearch->iSearchRank++;
192 if (!isset($aSearchTerm['word_id'])) {
193 $oSearch->iSearchRank++;
195 // also must not appear in the middle of the address
196 if (sizeof($this->aAddress) || sizeof($this->aAddressNonSearch)) {
197 $oSearch->iSearchRank++;
199 $aNewSearches[] = $oSearch;
201 } elseif ($sPhraseType == ''
202 && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null
204 // require a normalized exact match of the term
205 // if we have the normalizer version of the query
207 if ($this->iOperator == Operator::NONE
208 && (isset($aSearchTerm['word']) && $aSearchTerm['word'])
211 $oSearch = clone $this;
212 $oSearch->iSearchRank++;
214 $iOp = Operator::NEAR; // near == in for the moment
215 if ($aSearchTerm['operator'] == '') {
216 if (sizeof($this->aName)) {
217 $iOp = Operator::NAME;
219 $oSearch->iSearchRank += 2;
222 $oSearch->setPoiSearch($iOp, $aSearchTerm['class'], $aSearchTerm['type']);
223 $aNewSearches[] = $oSearch;
225 } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
226 $iWordID = $aSearchTerm['word_id'];
227 if (sizeof($this->aName)) {
228 if (($sPhraseType == '' || !$bFirstPhrase)
229 && $sPhraseType != 'country'
232 $oSearch = clone $this;
233 $oSearch->iSearchRank++;
234 $oSearch->aAddress[$iWordID] = $iWordID;
235 $aNewSearches[] = $oSearch;
237 $this->aFullNameAddress[$iWordID] = $iWordID;
240 $oSearch = clone $this;
241 $oSearch->iSearchRank++;
242 $oSearch->aName = array($iWordID => $iWordID);
243 $aNewSearches[] = $oSearch;
247 return $aNewSearches;
250 public function extendWithPartialTerm($aSearchTerm, $bStructuredPhrases, $iPhrase, &$aWordFrequencyScores, $aFullTokens)
252 // Only allow name terms.
253 if (!(isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])) {
257 $aNewSearches = array();
258 $iWordID = $aSearchTerm['word_id'];
260 if ((!$bStructuredPhrases || $iPhrase > 0)
261 && sizeof($this->aName)
262 && strpos($aSearchTerm['word_token'], ' ') === false
264 if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
265 $oSearch = clone $this;
266 $oSearch->iSearchRank++;
267 $oSearch->aAddress[$iWordID] = $iWordID;
268 $aNewSearches[] = $oSearch;
270 $oSearch = clone $this;
271 $oSearch->iSearchRank++;
272 $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
273 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
274 $oSearch->iSearchRank += 2;
276 if (sizeof($aFullTokens)) {
277 $oSearch->iSearchRank++;
279 $aNewSearches[] = $oSearch;
281 // revert to the token version?
282 foreach ($aFullTokens as $aSearchTermToken) {
283 if (empty($aSearchTermToken['country_code'])
284 && empty($aSearchTermToken['lat'])
285 && empty($aSearchTermToken['class'])
287 $oSearch = clone $this;
288 $oSearch->iSearchRank++;
289 $oSearch->aAddress[$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
290 $aNewSearches[] = $oSearch;
296 if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
297 && (!sizeof($this->aName) || $this->iNamePhrase == $iPhrase)
299 $oSearch = clone $this;
300 $oSearch->iSearchRank++;
301 if (!sizeof($this->aName)) {
302 $oSearch->iSearchRank += 1;
304 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
305 $oSearch->iSearchRank += 2;
307 if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
308 $oSearch->aName[$iWordID] = $iWordID;
310 $oSearch->aNameNonSearch[$iWordID] = $iWordID;
312 $oSearch->iNamePhrase = $iPhrase;
313 $aNewSearches[] = $oSearch;
316 return $aNewSearches;
319 /////////// Query functions
322 public function query(&$oDB, &$aWordFrequencyScores, &$aExactMatchCache, $iMinRank, $iMaxRank, $iLimit)
324 $aPlaceIDs = array();
327 if ($this->sCountryCode
328 && !sizeof($this->aName)
331 && !$this->oContext->hasNearPoint()
333 // Just looking for a country - look it up
334 if (4 >= $iMinRank && 4 <= $iMaxRank) {
335 $aPlaceIDs = $this->queryCountry($oDB);
337 } elseif (!sizeof($this->aName) && !sizeof($this->aAddress)) {
338 // Neither name nor address? Then we must be
339 // looking for a POI in a geographic area.
340 if ($this->oContext->isBoundedSearch()) {
341 $aPlaceIDs = $this->queryNearbyPoi($oDB, $iLimit);
343 } elseif ($this->iOperator == Operator::POSTCODE) {
344 // looking for postcode
345 $aPlaceIDs = $this->queryPostcode($oDB, $iLimit);
348 // First search for places according to name and address.
349 $aNamedPlaceIDs = $this->queryNamedPlace(
351 $aWordFrequencyScores,
357 if (sizeof($aNamedPlaceIDs)) {
358 foreach ($aNamedPlaceIDs as $aRow) {
359 $aPlaceIDs[] = $aRow['place_id'];
360 $aExactMatchCache[$aRow['place_id']] = $aRow['exactmatch'];
364 //now search for housenumber, if housenumber provided
365 if ($this->sHouseNumber && sizeof($aPlaceIDs)) {
366 $aResult = $this->queryHouseNumber($oDB, $aPlaceIDs, $iLimit);
368 if (sizeof($aResult)) {
369 $iHousenumber = $aResult['iHouseNumber'];
370 $aPlaceIDs = $aResult['aPlaceIDs'];
371 } elseif (!$this->looksLikeFullAddress()) {
372 $aPlaceIDs = array();
376 // finally get POIs if requested
377 if ($this->sClass && sizeof($aPlaceIDs)) {
378 $aPlaceIDs = $this->queryPoiByOperator($oDB, $aPlaceIDs, $iLimit);
383 echo "<br><b>Place IDs:</b> ";
384 var_Dump($aPlaceIDs);
387 if (sizeof($aPlaceIDs) && $this->sPostcode) {
388 $sSQL = 'SELECT place_id FROM placex';
389 $sSQL .= ' WHERE place_id in ('.join(',', $aPlaceIDs).')';
390 $sSQL .= " AND postcode = '".$this->sPostcode."'";
391 if (CONST_Debug) var_dump($sSQL);
392 $aFilteredPlaceIDs = chksql($oDB->getCol($sSQL));
393 if ($aFilteredPlaceIDs) {
394 $aPlaceIDs = $aFilteredPlaceIDs;
396 echo "<br><b>Place IDs after postcode filtering:</b> ";
397 var_Dump($aPlaceIDs);
402 return array('IDs' => $aPlaceIDs, 'houseNumber' => $iHousenumber);
406 private function queryCountry(&$oDB)
408 $sSQL = 'SELECT place_id FROM placex ';
409 $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
410 $sSQL .= ' AND rank_search = 4';
411 if ($this->oContext->bViewboxBounded) {
412 $sSQL .= ' AND ST_Intersects('.$this->oContext->sqlViewboxSmall.', geometry)';
414 $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
416 if (CONST_Debug) var_dump($sSQL);
418 return chksql($oDB->getCol($sSQL));
421 private function queryNearbyPoi(&$oDB, $iLimit)
423 if (!$this->sClass) {
427 $sPoiTable = $this->poiTable();
429 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = \''.$sPoiTable."'";
430 if (chksql($oDB->getOne($sSQL))) {
431 $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
432 if ($this->oContext->sqlCountryList) {
433 $sSQL .= ' JOIN placex USING (place_id)';
435 if ($this->oContext->hasNearPoint()) {
436 $sSQL .= ' WHERE '.$this->oContext->withinSQL('ct.centroid');
437 } elseif ($this->oContext->bViewboxBounded) {
438 $sSQL .= ' WHERE ST_Contains('.$this->oContext->sqlViewboxSmall.', ct.centroid)';
440 if ($this->oContext->sqlCountryList) {
441 $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList;
443 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
444 if ($this->oContext->sqlViewboxCentre) {
445 $sSQL .= ' ORDER BY ST_Distance(';
446 $sSQL .= $this->oContext->sqlViewboxCentre.', ct.centroid) ASC';
447 } elseif ($this->oContext->hasNearPoint()) {
448 $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('ct.centroid').' ASC';
450 $sSQL .= " limit $iLimit";
451 if (CONST_Debug) var_dump($sSQL);
452 return chksql($oDB->getCol($sSQL));
455 if ($this->oContext->hasNearPoint()) {
456 $sSQL = 'SELECT place_id FROM placex WHERE ';
457 $sSQL .= 'class=\''.$this->sClass."' and type='".$this->sType."'";
458 $sSQL .= ' AND '.$this->oContext->withinSQL('geometry');
459 $sSQL .= ' AND linked_place_id is null';
460 if ($this->oContext->sqlCountryList) {
461 $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList;
463 $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('centroid')." ASC";
464 $sSQL .= " LIMIT $iLimit";
465 if (CONST_Debug) var_dump($sSQL);
466 return chksql($oDB->getCol($sSQL));
472 private function queryPostcode(&$oDB, $iLimit)
474 $sSQL = 'SELECT p.place_id FROM location_postcode p ';
476 if (sizeof($this->aAddress)) {
477 $sSQL .= ', search_name s ';
478 $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
479 $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
480 $sSQL .= ' @> '.getArraySQL($this->aAddress).' AND ';
485 $sSQL .= "p.postcode = '".reset($this->aName)."'";
486 $sSQL .= $this->countryCodeSQL(' AND p.country_code');
487 $sSQL .= $this->oContext->excludeSQL(' AND p.place_id');
488 $sSQL .= " LIMIT $iLimit";
490 if (CONST_Debug) var_dump($sSQL);
492 return chksql($oDB->getCol($sSQL));
495 private function queryNamedPlace(&$oDB, $aWordFrequencyScores, $iMinAddressRank, $iMaxAddressRank, $iLimit)
500 if ($this->sHouseNumber && sizeof($this->aAddress)) {
501 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
503 $aOrder[0] .= 'EXISTS(';
504 $aOrder[0] .= ' SELECT place_id';
505 $aOrder[0] .= ' FROM placex';
506 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
507 $aOrder[0] .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
508 $aOrder[0] .= ' LIMIT 1';
510 // also housenumbers from interpolation lines table are needed
511 if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
512 $iHouseNumber = intval($this->sHouseNumber);
513 $aOrder[0] .= 'OR EXISTS(';
514 $aOrder[0] .= ' SELECT place_id ';
515 $aOrder[0] .= ' FROM location_property_osmline ';
516 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
517 $aOrder[0] .= ' AND startnumber is not NULL';
518 $aOrder[0] .= ' AND '.$iHouseNumber.'>=startnumber ';
519 $aOrder[0] .= ' AND '.$iHouseNumber.'<=endnumber ';
520 $aOrder[0] .= ' LIMIT 1';
523 $aOrder[0] .= ') DESC';
526 if (sizeof($this->aName)) {
527 $aTerms[] = 'name_vector @> '.getArraySQL($this->aName);
529 if (sizeof($this->aAddress)) {
530 // For infrequent name terms disable index usage for address
531 if (CONST_Search_NameOnlySearchFrequencyThreshold
532 && sizeof($this->aName) == 1
533 && $aWordFrequencyScores[$this->aName[reset($this->aName)]]
534 < CONST_Search_NameOnlySearchFrequencyThreshold
536 $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.getArraySQL($this->aAddress);
538 $aTerms[] = 'nameaddress_vector @> '.getArraySQL($this->aAddress);
542 $sCountryTerm = $this->countryCodeSQL('country_code');
544 $aTerms[] = $sCountryTerm;
547 if ($this->sHouseNumber) {
548 $aTerms[] = "address_rank between 16 and 27";
549 } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
550 if ($iMinAddressRank > 0) {
551 $aTerms[] = "address_rank >= ".$iMinAddressRank;
553 if ($iMaxAddressRank < 30) {
554 $aTerms[] = "address_rank <= ".$iMaxAddressRank;
558 if ($this->oContext->hasNearPoint()) {
559 $aTerms[] = $this->oContext->withinSQL('centroid');
560 $aOrder[] = $this->oContext->distanceSQL('centroid');
561 } elseif ($this->sPostcode) {
562 if (!sizeof($this->aAddress)) {
563 $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
565 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
569 $sExcludeSQL = $this->oContext->excludeSQL('place_id');
571 $aTerms[] = $sExcludeSQL;
574 if ($this->oContext->bViewboxBounded) {
575 $aTerms[] = 'centroid && '.$this->oContext->sqlViewboxSmall;
578 if ($this->oContext->hasNearPoint()) {
579 $aOrder[] = $this->oContext->distanceSQL('centroid');
582 if ($this->sHouseNumber) {
583 $sImportanceSQL = '- abs(26 - address_rank) + 3';
585 $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
587 $sImportanceSQL .= $this->oContext->viewboxImportanceSQL('centroid');
588 $aOrder[] = "$sImportanceSQL DESC";
590 if (sizeof($this->aFullNameAddress)) {
591 $sExactMatchSQL = ' ( ';
592 $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
593 $sExactMatchSQL .= ' SELECT unnest('.getArraySQL($this->aFullNameAddress).')';
594 $sExactMatchSQL .= ' INTERSECT ';
595 $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)';
596 $sExactMatchSQL .= ' ) s';
597 $sExactMatchSQL .= ') as exactmatch';
598 $aOrder[] = 'exactmatch DESC';
600 $sExactMatchSQL = '0::int as exactmatch';
603 if ($this->sHouseNumber || $this->sClass) {
607 if (sizeof($aTerms)) {
608 $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
609 $sSQL .= ' FROM search_name';
610 $sSQL .= ' WHERE '.join(' and ', $aTerms);
611 $sSQL .= ' ORDER BY '.join(', ', $aOrder);
612 $sSQL .= ' LIMIT '.$iLimit;
614 if (CONST_Debug) var_dump($sSQL);
618 "Could not get places for search terms."
625 private function queryHouseNumber(&$oDB, $aRoadPlaceIDs, $iLimit)
627 $sPlaceIDs = join(',', $aRoadPlaceIDs);
629 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
630 $sSQL = 'SELECT place_id FROM placex ';
631 $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
632 $sSQL .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
633 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
634 $sSQL .= " LIMIT $iLimit";
636 if (CONST_Debug) var_dump($sSQL);
638 $aPlaceIDs = chksql($oDB->getCol($sSQL));
640 if (sizeof($aPlaceIDs)) {
641 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
644 $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
645 $iHousenumber = intval($this->sHouseNumber);
646 if ($bIsIntHouseNumber) {
647 // if nothing found, search in the interpolation line table
648 $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
649 $sSQL .= ' WHERE startnumber is not NULL';
650 $sSQL .= ' AND parent_place_id in ('.$sPlaceIDs.') AND (';
651 if ($iHousenumber % 2 == 0) {
652 // If housenumber is even, look for housenumber in streets
653 // with interpolationtype even or all.
654 $sSQL .= "interpolationtype='even'";
656 // Else look for housenumber with interpolationtype odd or all.
657 $sSQL .= "interpolationtype='odd'";
659 $sSQL .= " or interpolationtype='all') and ";
660 $sSQL .= $iHousenumber.">=startnumber and ";
661 $sSQL .= $iHousenumber."<=endnumber";
662 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
663 $sSQL .= " limit $iLimit";
665 if (CONST_Debug) var_dump($sSQL);
667 $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
669 if (sizeof($aPlaceIDs)) {
670 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
674 // If nothing found try the aux fallback table
675 if (CONST_Use_Aux_Location_data) {
676 $sSQL = 'SELECT place_id FROM location_property_aux';
677 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
678 $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
679 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
680 $sSQL .= " limit $iLimit";
682 if (CONST_Debug) var_dump($sSQL);
684 $aPlaceIDs = chksql($oDB->getCol($sSQL));
686 if (sizeof($aPlaceIDs)) {
687 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
691 // If nothing found then search in Tiger data (location_property_tiger)
692 if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber) {
693 $sSQL = 'SELECT distinct place_id FROM location_property_tiger';
694 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
695 if ($iHousenumber % 2 == 0) {
696 $sSQL .= "interpolationtype='even'";
698 $sSQL .= "interpolationtype='odd'";
700 $sSQL .= " or interpolationtype='all') and ";
701 $sSQL .= $iHousenumber.">=startnumber and ";
702 $sSQL .= $iHousenumber."<=endnumber";
703 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
704 $sSQL .= " limit $iLimit";
706 if (CONST_Debug) var_dump($sSQL);
708 $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
710 if (sizeof($aPlaceIDs)) {
711 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
719 private function queryPoiByOperator(&$oDB, $aParentIDs, $iLimit)
721 $sPlaceIDs = join(',', $aParentIDs);
722 $aClassPlaceIDs = array();
724 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) {
725 // If they were searching for a named class (i.e. 'Kings Head pub')
726 // then we might have an extra match
727 $sSQL = 'SELECT place_id FROM placex ';
728 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
729 $sSQL .= " AND class='".$this->sClass."' ";
730 $sSQL .= " AND type='".$this->sType."'";
731 $sSQL .= " AND linked_place_id is null";
732 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
733 $sSQL .= " ORDER BY rank_search ASC ";
734 $sSQL .= " LIMIT $iLimit";
736 if (CONST_Debug) var_dump($sSQL);
738 $aClassPlaceIDs = chksql($oDB->getCol($sSQL));
741 // NEAR and IN are handled the same
742 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
743 $sClassTable = $this->poiTable();
744 $sSQL = "SELECT count(*) FROM pg_tables WHERE tablename = '$sClassTable'";
745 $bCacheTable = (bool) chksql($oDB->getOne($sSQL));
747 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
748 if (CONST_Debug) var_dump($sSQL);
749 $iMaxRank = (int)chksql($oDB->getOne($sSQL));
751 // For state / country level searches the normal radius search doesn't work very well
753 if ($iMaxRank < 9 && $bCacheTable) {
754 // Try and get a polygon to search in instead
755 $sSQL = 'SELECT geometry FROM placex';
756 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
757 $sSQL .= " AND rank_search < $iMaxRank + 5";
758 $sSQL .= " AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
759 $sSQL .= " ORDER BY rank_search ASC ";
761 if (CONST_Debug) var_dump($sSQL);
762 $sPlaceGeom = chksql($oDB->getOne($sSQL));
769 $sSQL = 'SELECT place_id FROM placex';
770 $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
771 if (CONST_Debug) var_dump($sSQL);
772 $aPlaceIDs = chksql($oDB->getCol($sSQL));
773 $sPlaceIDs = join(',', $aPlaceIDs);
776 if ($sPlaceIDs || $sPlaceGeom) {
779 // More efficient - can make the range bigger
783 if ($this->oContext->hasNearPoint()) {
784 $sOrderBySQL = $this->oContext->distanceSQL('l.centroid');
785 } elseif ($sPlaceIDs) {
786 $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
787 } elseif ($sPlaceGeom) {
788 $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
791 $sSQL = 'SELECT distinct i.place_id';
793 $sSQL .= ', i.order_term';
795 $sSQL .= ' from (SELECT l.place_id';
797 $sSQL .= ','.$sOrderBySQL.' as order_term';
799 $sSQL .= ' from '.$sClassTable.' as l';
802 $sSQL .= ",placex as f WHERE ";
803 $sSQL .= "f.place_id in ($sPlaceIDs) ";
804 $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
805 } elseif ($sPlaceGeom) {
806 $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
809 $sSQL .= $this->oContext->excludeSQL(' AND l.place_id');
810 $sSQL .= 'limit 300) i ';
812 $sSQL .= 'order by order_term asc';
814 $sSQL .= " limit $iLimit";
816 if (CONST_Debug) var_dump($sSQL);
818 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
820 if ($this->oContext->hasNearPoint()) {
821 $fRange = $this->oContext->nearRadius();
825 if ($this->oContext->hasNearPoint()) {
826 $sOrderBySQL = $this->oContext->distanceSQL('l.geometry');
828 $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
831 $sSQL = 'SELECT distinct l.place_id';
833 $sSQL .= ','.$sOrderBySQL.' as orderterm';
835 $sSQL .= ' FROM placex as l, placex as f';
836 $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
837 $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange)";
838 $sSQL .= " AND l.class='".$this->sClass."'";
839 $sSQL .= " AND l.type='".$this->sType."'";
840 $sSQL .= $this->oContext->excludeSQL(' AND l.place_id');
842 $sSQL .= "ORDER BY orderterm ASC";
844 $sSQL .= " limit $iLimit";
846 if (CONST_Debug) var_dump($sSQL);
848 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
853 return $aClassPlaceIDs;
856 private function poiTable()
858 return 'place_classtype_'.$this->sClass.'_'.$this->sType;
861 private function countryCodeSQL($sVar)
863 if ($this->sCountryCode) {
864 return $sVar.' = \''.$this->sCountryCode."'";
866 if ($this->oContext->sqlCountryList) {
867 return $sVar.' in '.$this->oContext->sqlCountryList;
873 /////////// Sort functions
876 public static function bySearchRank($a, $b)
878 if ($a->iSearchRank == $b->iSearchRank) {
879 return $a->iOperator + strlen($a->sHouseNumber)
880 - $b->iOperator - strlen($b->sHouseNumber);
883 return $a->iSearchRank < $b->iSearchRank ? -1 : 1;
886 //////////// Debugging functions
889 public function dumpAsHtmlTableRow(&$aWordIDs)
891 $kf = function ($k) use (&$aWordIDs) {
892 return $aWordIDs[$k];
896 echo "<td>$this->iSearchRank</td>";
897 echo "<td>".join(', ', array_map($kf, $this->aName))."</td>";
898 echo "<td>".join(', ', array_map($kf, $this->aNameNonSearch))."</td>";
899 echo "<td>".join(', ', array_map($kf, $this->aAddress))."</td>";
900 echo "<td>".join(', ', array_map($kf, $this->aAddressNonSearch))."</td>";
901 echo "<td>".$this->sCountryCode."</td>";
902 echo "<td>".Operator::toString($this->iOperator)."</td>";
903 echo "<td>".$this->sClass."</td>";
904 echo "<td>".$this->sType."</td>";
905 echo "<td>".$this->sPostcode."</td>";
906 echo "<td>".$this->sHouseNumber."</td>";