]> git.openstreetmap.org Git - nominatim.git/blob - website/search.php
recheck the rank limits before dropping out of the search loop - otherwise we can...
[nominatim.git] / website / search.php
1 <?php
2         @define('CONST_ConnectionBucket_PageType', 'Search');
3
4         require_once(dirname(dirname(__FILE__)).'/lib/init-website.php');
5         require_once(CONST_BasePath.'/lib/log.php');
6
7         ini_set('memory_limit', '200M');
8
9         $oDB =& getDB();
10
11         // Display defaults
12         $fLat = CONST_Default_Lat;
13         $fLon = CONST_Default_Lon;
14         $iZoom = CONST_Default_Zoom;
15         $bBoundingBoxSearch = isset($_GET['bounded'])?(bool)$_GET['bounded']:false;
16         $sOutputFormat = 'html';
17         $aSearchResults = array();
18         $aExcludePlaceIDs = array();
19         $sCountryCodesSQL = false;
20         $bDeDupe = isset($_GET['dedupe'])?(bool)$_GET['dedupe']:true;
21         $bReverseInPlan = false;
22         $iFinalLimit = isset($_GET['limit'])?(int)$_GET['limit']:10;
23         $iOffset = isset($_GET['offset'])?(int)$_GET['offset']:0;
24         $iMaxRank = 20;
25         if ($iFinalLimit > 50) $iFinalLimit = 50;
26         $iLimit = $iFinalLimit + min($iFinalLimit, 10);
27         $iMinAddressRank = 0;
28         $iMaxAddressRank = 30;
29         $aAddressRankList = array();
30         $sAllowedTypesSQLList = false;
31
32         // Format for output
33         if (isset($_GET['format']) && ($_GET['format'] == 'html' || $_GET['format'] == 'xml' || $_GET['format'] == 'json' ||  $_GET['format'] == 'jsonv2'))
34         {
35                 $sOutputFormat = $_GET['format'];
36         }
37
38         // Show / use polygons
39         $bShowPolygons = (boolean)isset($_GET['polygon']) && $_GET['polygon'];
40         if ($sOutputFormat == 'html')
41         {
42                 $bAsText = $bShowPolygons;
43                 $bShowPolygons = false;
44                 $bAsGeoJSON = false;
45                 $bAsKML = false;
46                 $bAsSVG = false;
47         }
48         else
49         {
50                 $bAsGeoJSON = (boolean)isset($_GET['polygon_geojson']) && $_GET['polygon_geojson'];
51                 $bAsKML = (boolean)isset($_GET['polygon_kml']) && $_GET['polygon_kml'];
52                 $bAsSVG = (boolean)isset($_GET['polygon_svg']) && $_GET['polygon_svg'];
53                 $bAsText = (boolean)isset($_GET['polygon_text']) && $_GET['polygon_text'];
54                 if ((($bShowPolygons?1:0)
55                                         + ($bAsGeoJSON?1:0)
56                                         + ($bAsKML?1:0)
57                                         + ($bAsSVG?1:0)
58                                         + ($bAsText?1:0)
59                         ) > CONST_PolygonOutput_MaximumTypes)
60                 {
61                         if (CONST_PolygonOutput_MaximumTypes)
62                         {
63                                 userError("Select only ".CONST_PolygonOutput_MaximumTypes." polgyon output option");
64                         }
65                         else
66                         {
67                                 userError("Polygon output is disabled");
68                         }
69                         exit;
70                 }
71         }
72
73         // Show address breakdown
74         $bShowAddressDetails = isset($_GET['addressdetails']) && $_GET['addressdetails'];
75
76         // Preferred language
77         $aLangPrefOrder = getPreferredLanguages();
78         if (isset($aLangPrefOrder['name:de'])) $bReverseInPlan = true;
79         if (isset($aLangPrefOrder['name:ru'])) $bReverseInPlan = true;
80         if (isset($aLangPrefOrder['name:ja'])) $bReverseInPlan = true;
81
82         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$aLangPrefOrder))."]";
83
84         if (isset($_GET['exclude_place_ids']) && $_GET['exclude_place_ids'])
85         {
86                 foreach(explode(',',$_GET['exclude_place_ids']) as $iExcludedPlaceID)
87                 {
88                         $iExcludedPlaceID = (int)$iExcludedPlaceID;
89                         if ($iExcludedPlaceID) $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
90                 }
91         }
92
93         // Only certain ranks of feature
94         if (isset($_GET['featureType']) && !isset($_GET['featuretype'])) $_GET['featuretype'] = $_GET['featureType'];
95
96         if (isset($_GET['featuretype']))
97         {
98                 switch($_GET['featuretype'])
99                 {
100                 case 'country':
101                         $iMinAddressRank = $iMaxAddressRank = 4;
102                         break;
103                 case 'state':
104                         $iMinAddressRank = $iMaxAddressRank = 8;
105                         break;
106                 case 'city':
107                         $iMinAddressRank = 14;
108                         $iMaxAddressRank = 16;
109                         break;
110                 case 'settlement':
111                         $iMinAddressRank = 8;
112                         $iMaxAddressRank = 20;
113                         break;
114                 }
115         }
116
117         if (isset($_GET['countrycodes']))
118         {
119                 $aCountryCodes = array();
120                 foreach(explode(',',$_GET['countrycodes']) as $sCountryCode)
121                 {
122                         if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
123                         {
124                                 $aCountryCodes[] = "'".strtolower($sCountryCode)."'";
125                         }
126                 }
127                 $sCountryCodesSQL = join(',', $aCountryCodes);
128         }
129
130         // Search query
131         $sQuery = (isset($_GET['q'])?trim($_GET['q']):'');
132         if (!$sQuery && isset($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'][0] == '/')
133         {
134                 $sQuery = substr($_SERVER['PATH_INFO'], 1);
135
136                 // reverse order of '/' separated string
137                 $aPhrases = explode('/', $sQuery);
138                 $aPhrases = array_reverse($aPhrases);
139                 $sQuery = join(', ',$aPhrases);
140         }
141
142         // Structured query?
143         $aStructuredOptions = array(
144                                 array('amenity', 26, 30, false),
145                                 array('street', 26, 30, false),
146                                 array('city', 14, 24, false),
147                                 array('postalcode', 5, 11, array(5, 11)),
148                                 array('county', 9, 13, false),
149                                 array('state', 8, 8, false),
150                                 array('country', 4, 4, false),
151                                 );
152         $aStructuredQuery = array();
153         $sAllowedTypesSQLList = '';
154         foreach($aStructuredOptions as $aStructuredOption)
155         {
156                 loadStructuredAddressElement($aStructuredQuery, $iMinAddressRank, $iMaxAddressRank, $aAddressRankList, $_GET, $aStructuredOption[0], $aStructuredOption[1], $aStructuredOption[2], $aStructuredOption[3]);
157         }
158         if (sizeof($aStructuredQuery) > 0) 
159         {
160                 $sQuery = join(', ', $aStructuredQuery);
161                 if ($iMaxAddressRank < 30)
162                 {
163                         $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
164                 }
165         }
166
167         if ($sQuery)
168         {
169                 $hLog = logStart($oDB, 'search', $sQuery, $aLangPrefOrder);
170
171                 // Hack to make it handle "new york, ny" (and variants) correctly
172                 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $sQuery);
173                 if (isset($aLangPrefOrder['name:en']))
174                 {
175                         $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
176                         $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
177                         $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
178                 }
179
180                 // If we have a view box create the SQL
181                 // Small is the actual view box, Large is double (on each axis) that
182                 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
183                 if (isset($_GET['viewboxlbrt']) && $_GET['viewboxlbrt'])
184                 {
185                         $aCoOrdinatesLBRT = explode(',',$_GET['viewboxlbrt']);
186                         $_GET['viewbox'] = $aCoOrdinatesLBRT[0].','.$aCoOrdinatesLBRT[3].','.$aCoOrdinatesLBRT[2].','.$aCoOrdinatesLBRT[1];
187                 }
188                 if (isset($_GET['viewbox']) && $_GET['viewbox'])
189                 {
190                         $aCoOrdinates = explode(',',$_GET['viewbox']);
191                         $sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aCoOrdinates[0].",".(float)$aCoOrdinates[1]."),ST_Point(".(float)$aCoOrdinates[2].",".(float)$aCoOrdinates[3].")),4326)";
192                         $fHeight = $aCoOrdinates[0]-$aCoOrdinates[2];
193                         $fWidth = $aCoOrdinates[1]-$aCoOrdinates[3];
194                         $aCoOrdinates[0] += $fHeight;
195                         $aCoOrdinates[2] -= $fHeight;
196                         $aCoOrdinates[1] += $fWidth;
197                         $aCoOrdinates[3] -= $fWidth;
198                         $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aCoOrdinates[0].",".(float)$aCoOrdinates[1]."),ST_Point(".(float)$aCoOrdinates[2].",".(float)$aCoOrdinates[3].")),4326)";
199                 }
200                 else
201                 {
202                         $bBoundingBoxSearch = false;
203                 }
204                 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
205                 {
206                         $aPoints = explode(',',$_GET['route']);
207                         if (sizeof($aPoints) % 2 != 0)
208                         {
209                                 userError("Uneven number of points");
210                                 exit;
211                         }
212                         $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
213                         $fPrevCoord = false;
214                         foreach($aPoints as $i => $fPoint)
215                         {
216                                 if ($i%2)
217                                 {
218                                         if ($i != 1) $sViewboxCentreSQL .= ",";
219                                         $sViewboxCentreSQL .= ((float)$fPoint).' '.$fPrevCoord;
220                                 }
221                                 else
222                                 {
223                                         $fPrevCoord = (float)$fPoint;
224                                 }
225                         }
226                         $sViewboxCentreSQL .= ")'::geometry,4326)";
227
228                         $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
229                         $sViewboxSmallSQL = $oDB->getOne($sSQL);
230                         if (PEAR::isError($sViewboxSmallSQL))
231                         {
232                                 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
233                         }
234                         $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
235
236                         $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
237                         $sViewboxLargeSQL = $oDB->getOne($sSQL);
238                         if (PEAR::isError($sViewboxLargeSQL))
239                         {
240                                 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
241                         }
242                         $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
243                         $bBoundingBoxSearch = true;
244                 }
245
246                 // Do we have anything that looks like a lat/lon pair?
247                 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
248                 {
249                         $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
250                         $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
251                         if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
252                         {
253                                 $_GET['nearlat'] = $fQueryLat;
254                                 $_GET['nearlon'] = $fQueryLon;
255                                 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
256                         }
257                 }
258                 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
259                 {
260                         $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
261                         $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
262                         if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
263                         {
264                                 $_GET['nearlat'] = $fQueryLat;
265                                 $_GET['nearlon'] = $fQueryLon;
266                                 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
267                         }
268                 }
269                 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9.]*)[, ]+(-?[0-9]+[0-9.]*)(\\]|$|\\b)/', $sQuery, $aData))
270                 {
271                         $fQueryLat = $aData[2];
272                         $fQueryLon = $aData[3];
273                         if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
274                         {
275                                 $_GET['nearlat'] = $fQueryLat;
276                                 $_GET['nearlon'] = $fQueryLon;
277                                 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
278                         }
279                 }
280
281                 if ($sQuery || $aStructuredQuery)
282                 {
283                         // Start with a blank search
284                         $aSearches = array(
285                                 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
286                                       'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
287                                       'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
288                         );
289
290                         $sNearPointSQL = false;
291                         if (isset($_GET['nearlat']) && isset($_GET['nearlon']))
292                         {
293                                 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$_GET['nearlon'].",".(float)$_GET['nearlat']."),4326)";
294                                 $aSearches[0]['fLat'] = (float)$_GET['nearlat'];
295                                 $aSearches[0]['fLon'] = (float)$_GET['nearlon'];
296                                 $aSearches[0]['fRadius'] = 0.1;
297                         }
298
299                         $bSpecialTerms = false;
300                         preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
301                         $aSpecialTerms = array();
302                         foreach($aSpecialTermsRaw as $aSpecialTerm)
303                         {
304                                 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
305                                 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
306                         }
307
308                         preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
309                         $aSpecialTerms = array();
310                         if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
311                         {
312                                 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
313                                 unset($aStructuredQuery['amenity']);
314                         }
315                         foreach($aSpecialTermsRaw as $aSpecialTerm)
316                         {
317                                 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
318                                 $sToken = $oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
319                                 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
320                                 $sSQL .= ' from word where word_token in (\' '.$sToken.'\')) as x where (class is not null and class not in (\'place\')) or country_code is not null';
321                                 if (CONST_Debug) var_Dump($sSQL);
322                                 $aSearchWords = $oDB->getAll($sSQL);
323                                 $aNewSearches = array();
324                                 foreach($aSearches as $aSearch)
325                                 {
326                                         foreach($aSearchWords as $aSearchTerm)
327                                         {
328                                                 $aNewSearch = $aSearch;
329                                                 if ($aSearchTerm['country_code'])
330                                                 {
331                                                         $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
332                                                         $aNewSearches[] = $aNewSearch;
333                                                         $bSpecialTerms = true;
334                                                 }
335                                                 if ($aSearchTerm['class'])
336                                                 {
337                                                         $aNewSearch['sClass'] = $aSearchTerm['class'];
338                                                         $aNewSearch['sType'] = $aSearchTerm['type'];
339                                                         $aNewSearches[] = $aNewSearch;
340                                                         $bSpecialTerms = true;
341                                                 }
342                                         }
343                                 }
344                                 $aSearches = $aNewSearches;
345                         }
346
347                         // Split query into phrases
348                         // Commas are used to reduce the search space by indicating where phrases split
349                         if (sizeof($aStructuredQuery) > 0)
350                         {
351                                 $aPhrases = $aStructuredQuery;
352                                 $bStructuredPhrases = true;
353                         }
354                         else
355                         {
356                                 $aPhrases = explode(',',$sQuery);
357                                 $bStructuredPhrases = false;
358                         }
359
360                         // Convert each phrase to standard form
361                         // Create a list of standard words
362                         // Get all 'sets' of words
363                         // Generate a complete list of all
364                         $aTokens = array();
365                         foreach($aPhrases as $iPhrase => $sPhrase)
366                         {
367                                 $aPhrase = $oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
368                                 if (PEAR::isError($aPhrase))
369                                 {
370                                         userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
371                                         if (CONST_Debug) var_dump($aPhrase);
372                                         exit;
373                                 }
374                                 if (trim($aPhrase['string']))
375                                 {
376                                         $aPhrases[$iPhrase] = $aPhrase;
377                                         $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
378                                         $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
379                                         $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
380                                 }
381                                 else
382                                 {
383                                         unset($aPhrases[$iPhrase]);
384                                 }
385                         }
386
387                         // reindex phrases - we make assumptions later on
388                         $aPhraseTypes = array_keys($aPhrases);
389                         $aPhrases = array_values($aPhrases);
390
391                         if (sizeof($aTokens))
392                         {
393
394                                 // Check which tokens we have, get the ID numbers
395                                 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
396                                 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
397                                 //$sSQL .= ' and search_name_count < '.CONST_Max_Word_Frequency;
398                                 //$sSQL .= ' group by word_token, word, class, type, country_code';
399
400                                 if (CONST_Debug) var_Dump($sSQL);
401
402                                 $aValidTokens = array();
403                                 if (sizeof($aTokens)) $aDatabaseWords = $oDB->getAll($sSQL);
404                                 else $aDatabaseWords = array();
405                                 if (PEAR::IsError($aDatabaseWords))
406                                 {
407                                         failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
408                                 }
409                                 $aPossibleMainWordIDs = array();
410                                 $aWordFrequencyScores = array();
411                                 foreach($aDatabaseWords as $aToken)
412                                 {
413                                         // Very special case - require 2 letter country param to match the country code found
414                                         if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
415                                                         && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
416                                         {
417                                                 continue;
418                                         }
419
420                                         if (isset($aValidTokens[$aToken['word_token']]))
421                                         {
422                                                 $aValidTokens[$aToken['word_token']][] = $aToken;
423                                         }
424                                         else
425                                         {
426                                                 $aValidTokens[$aToken['word_token']] = array($aToken);
427                                         }
428                                         if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
429                                         $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
430                                 }
431                                 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
432
433                                 // Try and calculate GB postcodes we might be missing
434                                 foreach($aTokens as $sToken)
435                                 {
436                                         // Source of gb postcodes is now definitive - always use
437                                         if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
438                                         {
439                                                 if (substr($aData[1],-2,1) != ' ')
440                                                 {
441                                                         $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
442                                                         $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
443                                                 }
444                                                 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $oDB);
445                                                 if ($aGBPostcodeLocation)
446                                                 {
447                                                         $aValidTokens[$sToken] = $aGBPostcodeLocation;
448                                                 }
449                                         }
450                                 }
451
452                                 foreach($aTokens as $sToken)
453                                 {
454                                         // Unknown single word token with a number - assume it is a house number
455                                         if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
456                                         {
457                                                 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
458                                         }
459                                 }
460
461                                 // Any words that have failed completely?
462                                 // TODO: suggestions
463
464                                 // Start the search process
465                                 $aResultPlaceIDs = array();
466
467                                 /*
468                                    Calculate all searches using aValidTokens i.e.
469
470                                    'Wodsworth Road, Sheffield' =>
471
472                                    Phrase Wordset
473                                    0      0       (wodsworth road)
474                                    0      1       (wodsworth)(road)
475                                    1      0       (sheffield)
476
477                                    Score how good the search is so they can be ordered
478                                  */
479                                 foreach($aPhrases as $iPhrase => $sPhrase)
480                                 {
481                                         $aNewPhraseSearches = array();
482                                         if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
483                                         else $sPhraseType = '';
484
485                                         foreach($aPhrases[$iPhrase]['wordsets'] as $aWordset)
486                                         {
487                                                 $aWordsetSearches = $aSearches;
488
489                                                 // Add all words from this wordset
490                                                 foreach($aWordset as $iToken => $sToken)
491                                                 {
492                                                         //echo "<br><b>$sToken</b>";
493                                                         $aNewWordsetSearches = array();
494
495                                                         foreach($aWordsetSearches as $aCurrentSearch)
496                                                         {
497                                                                 //echo "<i>";
498                                                                 //var_dump($aCurrentSearch);
499                                                                 //echo "</i>";
500
501                                                                 // If the token is valid
502                                                                 if (isset($aValidTokens[' '.$sToken]))
503                                                                 {
504                                                                         foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
505                                                                         {
506                                                                                 $aSearch = $aCurrentSearch;
507                                                                                 $aSearch['iSearchRank']++;
508                                                                                 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
509                                                                                 {
510                                                                                         if ($aSearch['sCountryCode'] === false)
511                                                                                         {
512                                                                                                 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
513                                                                                                 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
514                                                                                                 // If reverse order is enabled, it may appear at the beginning as well.
515                                                                                                 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
516                                                                                                                 (!$bReverseInPlan || $iToken > 0 || $iPhrase > 0))
517                                                                                                 {
518                                                                                                         $aSearch['iSearchRank'] += 5;
519                                                                                                 }
520                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
521                                                                                         }
522                                                                                 }
523                                                                                 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
524                                                                                 {
525                                                                                         if ($aSearch['fLat'] === '')
526                                                                                         {
527                                                                                                 $aSearch['fLat'] = $aSearchTerm['lat'];
528                                                                                                 $aSearch['fLon'] = $aSearchTerm['lon'];
529                                                                                                 $aSearch['fRadius'] = $aSearchTerm['radius'];
530                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
531                                                                                         }
532                                                                                 }
533                                                                                 elseif ($sPhraseType == 'postalcode')
534                                                                                 {
535                                                                                         // 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
536                                                                                         if (sizeof($aSearch['aName']))
537                                                                                         {
538                                                                                                 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
539                                                                                                 $aSearch['aName'] = array();
540                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
541                                                                                         }
542                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
543                                                                                 }
544                                                                                 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
545                                                                                 {
546                                                                                         if ($aSearch['sHouseNumber'] === '')
547                                                                                         {
548                                                                                                 $aSearch['sHouseNumber'] = $sToken;
549                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
550                                                                                                 /*
551                                                                                                 // Fall back to not searching for this item (better than nothing)
552                                                                                                 $aSearch = $aCurrentSearch;
553                                                                                                 $aSearch['iSearchRank'] += 1;
554                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
555                                                                                                  */
556                                                                                         }
557                                                                                 }
558                                                                                 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
559                                                                                 {
560                                                                                         if ($aSearch['sClass'] === '')
561                                                                                         {
562                                                                                                 $aSearch['sOperator'] = $aSearchTerm['operator'];
563                                                                                                 $aSearch['sClass'] = $aSearchTerm['class'];
564                                                                                                 $aSearch['sType'] = $aSearchTerm['type'];
565                                                                                                 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
566                                                                                                 else $aSearch['sOperator'] = 'near'; // near = in for the moment
567
568                                                                                                 // Do we have a shortcut id?
569                                                                                                 if ($aSearch['sOperator'] == 'name')
570                                                                                                 {
571                                                                                                         $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
572                                                                                                         if ($iAmenityID = $oDB->getOne($sSQL))
573                                                                                                         {
574                                                                                                                 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
575                                                                                                                 $aSearch['aName'][$iAmenityID] = $iAmenityID;
576                                                                                                                 $aSearch['sClass'] = '';
577                                                                                                                 $aSearch['sType'] = '';
578                                                                                                         }
579                                                                                                 }
580                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
581                                                                                         }
582                                                                                 }
583                                                                                 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
584                                                                                 {
585                                                                                         if (sizeof($aSearch['aName']))
586                                                                                         {
587                                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
588                                                                                                 {
589                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
590                                                                                                 }
591                                                                                                 else
592                                                                                                 {
593                                                                                                         $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
594                                                                                                         $aSearch['iSearchRank'] += 1000; // skip;
595                                                                                                 }
596                                                                                         }
597                                                                                         else
598                                                                                         {
599                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
600                                                                                                 //$aSearch['iNamePhrase'] = $iPhrase;
601                                                                                         }
602                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
603                                                                                 }
604                                                                         }
605                                                                 }
606                                                                 if (isset($aValidTokens[$sToken]))
607                                                                 {
608                                                                         // Allow searching for a word - but at extra cost
609                                                                         foreach($aValidTokens[$sToken] as $aSearchTerm)
610                                                                         {
611                                                                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
612                                                                                 {
613                                                                                         if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
614                                                                                         {
615                                                                                                 $aSearch = $aCurrentSearch;
616                                                                                                 $aSearch['iSearchRank'] += 1;
617                                                                                                 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
618                                                                                                 {
619                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
620                                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
621                                                                                                 }
622                                                                                                 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
623                                                                                                 {
624                                                                                                         foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
625                                                                                                         {
626                                                                                                                 if (empty($aSearchTermToken['country_code'])
627                                                                                                                                 && empty($aSearchTermToken['lat'])
628                                                                                                                                 && empty($aSearchTermToken['class']))
629                                                                                                                 {
630                                                                                                                         $aSearch = $aCurrentSearch;
631                                                                                                                         $aSearch['iSearchRank'] += 1;
632                                                                                                                         $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
633                                                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
634                                                                                                                 }
635                                                                                                         }
636                                                                                                 }
637                                                                                                 else
638                                                                                                 {
639                                                                                                         $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
640                                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
641                                                                                                 }
642                                                                                         }
643
644                                                                                         if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
645                                                                                         {
646                                                                                                 $aSearch = $aCurrentSearch;
647                                                                                                 $aSearch['iSearchRank'] += 2;
648                                                                                                 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
649                                                                                                 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
650                                                                                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
651                                                                                                 else
652                                                                                                         $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
653                                                                                                 $aSearch['iNamePhrase'] = $iPhrase;
654                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
655                                                                                         }
656                                                                                 }
657                                                                         }
658                                                                 }
659                                                                 else
660                                                                 {
661                                                                         // Allow skipping a word - but at EXTREAM cost
662                                                                         //$aSearch = $aCurrentSearch;
663                                                                         //$aSearch['iSearchRank']+=100;
664                                                                         //$aNewWordsetSearches[] = $aSearch;
665                                                                 }
666                                                         }
667                                                         // Sort and cut
668                                                         usort($aNewWordsetSearches, 'bySearchRank');
669                                                         $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
670                                                 }
671                                                 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
672
673                                                 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
674                                                 usort($aNewPhraseSearches, 'bySearchRank');
675
676                                                 $aSearchHash = array();
677                                                 foreach($aNewPhraseSearches as $iSearch => $aSearch)
678                                                 {
679                                                         $sHash = serialize($aSearch);
680                                                         if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
681                                                         else $aSearchHash[$sHash] = 1;
682                                                 }
683
684                                                 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
685                                         }
686
687                                         // Re-group the searches by their score, junk anything over 20 as just not worth trying
688                                         $aGroupedSearches = array();
689                                         foreach($aNewPhraseSearches as $aSearch)
690                                         {
691                                                 if ($aSearch['iSearchRank'] < $iMaxRank)
692                                                 {
693                                                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
694                                                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
695                                                 }
696                                         }
697                                         ksort($aGroupedSearches);
698
699                                         $iSearchCount = 0;
700                                         $aSearches = array();
701                                         foreach($aGroupedSearches as $iScore => $aNewSearches)
702                                         {
703                                                 $iSearchCount += sizeof($aNewSearches);
704                                                 $aSearches = array_merge($aSearches, $aNewSearches);
705                                                 if ($iSearchCount > 50) break;
706                                         }
707
708                                         //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
709
710                                 }
711                         }
712                         else
713                         {
714                                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
715                                 $aGroupedSearches = array();
716                                 foreach($aSearches as $aSearch)
717                                 {
718                                         if ($aSearch['iSearchRank'] < $iMaxRank)
719                                         {
720                                                 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
721                                                 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
722                                         }
723                                 }
724                                 ksort($aGroupedSearches);
725                         }
726
727                         if (CONST_Debug) var_Dump($aGroupedSearches);
728
729                         if ($bReverseInPlan)
730                         {
731                                 $aCopyGroupedSearches = $aGroupedSearches;
732                                 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
733                                 {
734                                         foreach($aSearches as $iSearch => $aSearch)
735                                         {
736                                                 if (sizeof($aSearch['aAddress']))
737                                                 {
738                                                         $iReverseItem = array_pop($aSearch['aAddress']);
739                                                         if (isset($aPossibleMainWordIDs[$iReverseItem]))
740                                                         {
741                                                                 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
742                                                                 $aSearch['aName'] = array($iReverseItem);
743                                                                 $aGroupedSearches[$iGroup][] = $aSearch;
744                                                         }
745                                                         //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
746                                                         //$aGroupedSearches[$iGroup][] = $aReverseSearch;
747                                                 }
748                                         }
749                                 }
750                         }
751
752                         if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
753                         {
754                                 $aCopyGroupedSearches = $aGroupedSearches;
755                                 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
756                                 {
757                                         foreach($aSearches as $iSearch => $aSearch)
758                                         {
759                                                 $aReductionsList = array($aSearch['aAddress']);
760                                                 $iSearchRank = $aSearch['iSearchRank'];
761                                                 while(sizeof($aReductionsList) > 0)
762                                                 {
763                                                         $iSearchRank += 5;
764                                                         if ($iSearchRank > iMaxRank) break 3;
765                                                         $aNewReductionsList = array();
766                                                         foreach($aReductionsList as $aReductionsWordList)
767                                                         {
768                                                                 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
769                                                                 {
770                                                                         $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
771                                                                         $aReverseSearch = $aSearch;
772                                                                         $aSearch['aAddress'] = $aReductionsWordListResult;
773                                                                         $aSearch['iSearchRank'] = $iSearchRank;
774                                                                         $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
775                                                                         if (sizeof($aReductionsWordListResult) > 0)
776                                                                         {
777                                                                                 $aNewReductionsList[] = $aReductionsWordListResult;
778                                                                         }
779                                                                 }
780                                                         }
781                                                         $aReductionsList = $aNewReductionsList;
782                                                 }
783                                         }
784                                 }
785                                 ksort($aGroupedSearches);
786                         }
787
788                         // Filter out duplicate searches
789                         $aSearchHash = array();
790                         foreach($aGroupedSearches as $iGroup => $aSearches)
791                         {
792                                 foreach($aSearches as $iSearch => $aSearch)
793                                 {
794                                         $sHash = serialize($aSearch);
795                                         if (isset($aSearchHash[$sHash]))
796                                         {
797                                                 unset($aGroupedSearches[$iGroup][$iSearch]);
798                                                 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
799                                         }
800                                         else
801                                         {
802                                                 $aSearchHash[$sHash] = 1;
803                                         }
804                                 }
805                         }
806
807                         if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
808
809                         $iGroupLoop = 0;
810                         $iQueryLoop = 0;
811                         foreach($aGroupedSearches as $iGroupedRank => $aSearches)
812                         {
813                                 $iGroupLoop++;
814                                 foreach($aSearches as $aSearch)
815                                 {
816                                         $iQueryLoop++;
817
818                                         if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
819                                         if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
820
821
822                                         // Must have a location term
823                                         if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
824                                         {
825                                                 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
826                                                 {
827                                                         if (4 >= $iMinAddressRank && 4 <= $iMaxAddressRank)
828                                                         {
829                                                                 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
830                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
831                                                                 $sSQL .= " order by st_area(geometry) desc limit 1";
832                                                                 if (CONST_Debug) var_dump($sSQL);
833                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
834                                                         }
835                                                 }
836                                                 else
837                                                 {
838                                                         if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
839                                                         if (!$aSearch['sClass']) continue;
840                                                         $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
841                                                         if ($oDB->getOne($sSQL))
842                                                         {
843                                                                 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
844                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
845                                                                 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
846                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
847                                                                 if (sizeof($aExcludePlaceIDs))
848                                                                 {
849                                                                         $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
850                                                                 }
851                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
852                                                                 $sSQL .= " limit $iLimit";
853                                                                 if (CONST_Debug) var_dump($sSQL);
854                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
855
856                                                                 // If excluded place IDs are given, it is fair to assume that
857                                                                 // there have been results in the small box, so no further
858                                                                 // expansion in that case.
859                                                                 if (!sizeof($aPlaceIDs) && !sizeof($aExcludePlaceIDs))
860                                                                 {
861                                                                         $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
862                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
863                                                                         $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
864                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
865                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
866                                                                         $sSQL .= " limit $iLimit";
867                                                                         if (CONST_Debug) var_dump($sSQL);
868                                                                         $aPlaceIDs = $oDB->getCol($sSQL);
869                                                                 }
870                                                         }
871                                                         else
872                                                         {
873                                                                 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
874                                                                 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
875                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
876                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
877                                                                 $sSQL .= " limit $iLimit";
878                                                                 if (CONST_Debug) var_dump($sSQL);
879                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
880                                                         }
881                                                 }
882                                         }
883                                         else
884                                         {
885                                                 $aPlaceIDs = array();
886
887                                                 // First we need a position, either aName or fLat or both
888                                                 $aTerms = array();
889                                                 $aOrder = array();
890
891                                                 // TODO: filter out the pointless search terms (2 letter name tokens and less)
892                                                 // they might be right - but they are just too darned expensive to run
893                                                 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
894                                                 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
895                                                 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
896                                                 {
897                                                         // For infrequent name terms disable index usage for address
898                                                         if (CONST_Search_NameOnlySearchFrequencyThreshold &&
899                                                                         sizeof($aSearch['aName']) == 1 &&
900                                                                         $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
901                                                         {
902                                                                 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
903                                                         }
904                                                         else
905                                                         {
906                                                                 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
907                                                                 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
908                                                         }
909                                                 }
910                                                 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
911                                                 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
912                                                 if ($aSearch['fLon'] && $aSearch['fLat'])
913                                                 {
914                                                         $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
915                                                         $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
916                                                 }
917                                                 if (sizeof($aExcludePlaceIDs))
918                                                 {
919                                                         $aTerms[] = "place_id not in (".join(',',$aExcludePlaceIDs).")";
920                                                 }
921                                                 if ($sCountryCodesSQL)
922                                                 {
923                                                         $aTerms[] = "country_code in ($sCountryCodesSQL)";
924                                                 }
925
926                                                 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
927                                                 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
928
929                                                 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
930                                                 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
931                                                 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
932                                                 $aOrder[] = "$sImportanceSQL DESC";
933                                                 if (sizeof($aSearch['aFullNameAddress']))
934                                                 {
935                                                         $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
936                                                 }
937
938                                                 if (sizeof($aTerms))
939                                                 {
940                                                         $sSQL = "select place_id";
941                                                         $sSQL .= " from search_name";
942                                                         $sSQL .= " where ".join(' and ',$aTerms);
943                                                         $sSQL .= " order by ".join(', ',$aOrder);
944                                                         if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
945                                                                 $sSQL .= " limit 50";
946                                                         elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
947                                                                 $sSQL .= " limit 1";
948                                                         else
949                                                                 $sSQL .= " limit ".$iLimit;
950
951                                                         if (CONST_Debug) { var_dump($sSQL); }
952                                                         $aViewBoxPlaceIDs = $oDB->getAll($sSQL);
953                                                         if (PEAR::IsError($aViewBoxPlaceIDs))
954                                                         {
955                                                                 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
956                                                         }
957                                                         //var_dump($aViewBoxPlaceIDs);
958                                                         // Did we have an viewbox matches?
959                                                         $aPlaceIDs = array();
960                                                         $bViewBoxMatch = false;
961                                                         foreach($aViewBoxPlaceIDs as $aViewBoxRow)
962                                                         {
963                                                                 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
964                                                                 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
965                                                                 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
966                                                                 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
967                                                                 $aPlaceIDs[] = $aViewBoxRow['place_id'];
968                                                         }
969                                                 }
970                                                 //var_Dump($aPlaceIDs);
971                                                 //exit;
972
973                                                 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
974                                                 {
975                                                         $aRoadPlaceIDs = $aPlaceIDs;
976                                                         $sPlaceIDs = join(',',$aPlaceIDs);
977
978                                                         // Now they are indexed look for a house attached to a street we found
979                                                         $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
980                                                         $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
981                                                         if (sizeof($aExcludePlaceIDs))
982                                                         {
983                                                                 $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
984                                                         }
985                                                         $sSQL .= " limit $iLimit";
986                                                         if (CONST_Debug) var_dump($sSQL);
987                                                         $aPlaceIDs = $oDB->getCol($sSQL);
988
989                                                         // If not try the aux fallback table
990                                                         if (!sizeof($aPlaceIDs))
991                                                         {
992                                                                 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
993                                                                 if (sizeof($aExcludePlaceIDs))
994                                                                 {
995                                                                         $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
996                                                                 }
997                                                                 //$sSQL .= " limit $iLimit";
998                                                                 if (CONST_Debug) var_dump($sSQL);
999                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
1000                                                         }
1001
1002                                                         if (!sizeof($aPlaceIDs))
1003                                                         {
1004                                                                 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1005                                                                 if (sizeof($aExcludePlaceIDs))
1006                                                                 {
1007                                                                         $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
1008                                                                 }
1009                                                                 //$sSQL .= " limit $iLimit";
1010                                                                 if (CONST_Debug) var_dump($sSQL);
1011                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
1012                                                         }
1013
1014                                                         // Fallback to the road
1015                                                         if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1016                                                         {
1017                                                                 $aPlaceIDs = $aRoadPlaceIDs;
1018                                                         }
1019
1020                                                 }
1021
1022                                                 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1023                                                 {
1024                                                         $sPlaceIDs = join(',',$aPlaceIDs);
1025                                                         $aClassPlaceIDs = array();
1026
1027                                                         if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1028                                                         {
1029                                                                 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1030                                                                 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1031                                                                 $sSQL .= " and linked_place_id is null";
1032                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1033                                                                 $sSQL .= " order by rank_search asc limit $iLimit";
1034                                                                 if (CONST_Debug) var_dump($sSQL);
1035                                                                 $aClassPlaceIDs = $oDB->getCol($sSQL);
1036                                                         }
1037
1038                                                         if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1039                                                         {
1040                                                                 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1041                                                                 $bCacheTable = $oDB->getOne($sSQL);
1042
1043                                                                 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1044
1045                                                                 if (CONST_Debug) var_dump($sSQL);
1046                                                                 $iMaxRank = ((int)$oDB->getOne($sSQL));
1047
1048                                                                 // For state / country level searches the normal radius search doesn't work very well
1049                                                                 $sPlaceGeom = false;
1050                                                                 if ($iMaxRank < 9 && $bCacheTable)
1051                                                                 {
1052                                                                         // Try and get a polygon to search in instead
1053                                                                         $sSQL = "select geometry from placex where place_id in ($sPlaceIDs) and rank_search < $iMaxRank + 5 and st_geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon') order by rank_search asc limit 1";
1054                                                                         if (CONST_Debug) var_dump($sSQL);
1055                                                                         $sPlaceGeom = $oDB->getOne($sSQL);
1056                                                                 }
1057
1058                                                                 if ($sPlaceGeom)
1059                                                                 {
1060                                                                         $sPlaceIDs = false;
1061                                                                 }
1062                                                                 else
1063                                                                 {
1064                                                                         $iMaxRank += 5;
1065                                                                         $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
1066                                                                         if (CONST_Debug) var_dump($sSQL);
1067                                                                         $aPlaceIDs = $oDB->getCol($sSQL);
1068                                                                         $sPlaceIDs = join(',',$aPlaceIDs);
1069                                                                 }
1070
1071                                                                 if ($sPlaceIDs || $sPlaceGeom)
1072                                                                 {
1073
1074                                                                         $fRange = 0.01;
1075                                                                         if ($bCacheTable)
1076                                                                         {
1077                                                                                 // More efficient - can make the range bigger
1078                                                                                 $fRange = 0.05;
1079
1080                                                                                 $sOrderBySQL = '';
1081                                                                                 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1082                                                                                 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1083                                                                                 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1084
1085                                                                                 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1086                                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1087                                                                                 if ($sPlaceIDs)
1088                                                                                 {
1089                                                                                         $sSQL .= ",placex as f where ";
1090                                                                                         $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1091                                                                                 }
1092                                                                                 if ($sPlaceGeom)
1093                                                                                 {
1094                                                                                         $sSQL .= " where ";
1095                                                                                         $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1096                                                                                 }
1097                                                                                 if (sizeof($aExcludePlaceIDs))
1098                                                                                 {
1099                                                                                         $sSQL .= " and l.place_id not in (".join(',',$aExcludePlaceIDs).")";
1100                                                                                 }
1101                                                                                 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1102                                                                                 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1103                                                                                 if ($iOffset) $sSQL .= " offset $iOffset";
1104                                                                                 $sSQL .= " limit $iLimit";
1105                                                                                 if (CONST_Debug) var_dump($sSQL);
1106                                                                                 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $oDB->getCol($sSQL));
1107                                                                         }
1108                                                                         else
1109                                                                         {
1110                                                                                 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1111
1112                                                                                 $sOrderBySQL = '';
1113                                                                                 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1114                                                                                 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1115
1116                                                                                 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1117                                                                                 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1118                                                                                 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1119                                                                                 if (sizeof($aExcludePlaceIDs))
1120                                                                                 {
1121                                                                                         $sSQL .= " and l.place_id not in (".join(',',$aExcludePlaceIDs).")";
1122                                                                                 }
1123                                                                                 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1124                                                                                 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1125                                                                                 if ($iOffset) $sSQL .= " offset $iOffset";
1126                                                                                 $sSQL .= " limit $iLimit";
1127                                                                                 if (CONST_Debug) var_dump($sSQL);
1128                                                                                 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $oDB->getCol($sSQL));
1129                                                                         }
1130                                                                 }
1131                                                         }
1132
1133                                                         $aPlaceIDs = $aClassPlaceIDs;
1134
1135                                                 }
1136
1137                                         }
1138
1139                                         if (PEAR::IsError($aPlaceIDs))
1140                                         {
1141                                                 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1142                                         }
1143
1144                                         if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1145
1146                                         foreach($aPlaceIDs as $iPlaceID)
1147                                         {
1148                                                 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1149                                         }
1150                                         if ($iQueryLoop > 20) break;
1151                                 }
1152
1153                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($iMinAddressRank != 0 || $iMaxAddressRank != 30))
1154                                 {
1155                                         // Need to verify passes rank limits before dropping out of the loop (yuk!)
1156                                         $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1157                                         $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
1158                                         if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1159                                         if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1160                                         $sSQL .= ") ";
1161                                         if (CONST_Debug) var_dump($sSQL);
1162                                         $aResultPlaceIDs = $oDB->getCol($sSQL);
1163                                 }
1164
1165
1166                                 //exit;
1167                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1168                                 if ($iGroupLoop > 4) break;
1169                                 if ($iQueryLoop > 30) break;
1170                         }
1171
1172                         // Did we find anything?
1173                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1174                         {
1175                                 //var_Dump($aResultPlaceIDs);exit;
1176                                 // Get the details for display (is this a redundant extra step?)
1177                                 $sPlaceIDs = join(',',$aResultPlaceIDs);
1178                                 $sOrderSQL = 'CASE ';
1179                                 foreach(array_keys($aResultPlaceIDs) as $iOrder => $iPlaceID)
1180                                 {
1181                                         $sOrderSQL .= 'when min(place_id) = '.$iPlaceID.' then '.$iOrder.' ';
1182                                 }
1183                                 $sOrderSQL .= ' ELSE 10000000 END';
1184                                 $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id,calculated_country_code as country_code,";
1185                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1186                                 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
1187                                 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
1188                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1189                                 //$sSQL .= $sOrderSQL." as porder, ";
1190                                 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
1191                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(placex.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
1192                                 $sSQL .= "(extratags->'place') as extra_place ";
1193                                 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
1194                                 $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
1195                                 if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1196                                 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1197                                 $sSQL .= ") ";
1198                                 if ($sAllowedTypesSQLList) $sSQL .= "and placex.class in $sAllowedTypesSQLList ";
1199                                 $sSQL .= "and linked_place_id is null ";
1200                                 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
1201                                 if (!$bDeDupe) $sSQL .= ",place_id";
1202                                 $sSQL .= ",langaddress ";
1203                                 $sSQL .= ",placename ";
1204                                 $sSQL .= ",ref ";
1205                                 $sSQL .= ",extratags->'place' ";
1206                                 $sSQL .= " union ";
1207                                 $sSQL .= "select 'T' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id,'us' as country_code,";
1208                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1209                                 $sSQL .= "null as placename,";
1210                                 $sSQL .= "null as ref,";
1211                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1212                                 //$sSQL .= $sOrderSQL." as porder, ";
1213                                 $sSQL .= "-0.15 as importance, ";
1214                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_tiger.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
1215                                 $sSQL .= "null as extra_place ";
1216                                 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
1217                                 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1218                                 $sSQL .= "group by place_id";
1219                                 if (!$bDeDupe) $sSQL .= ",place_id";
1220                                 $sSQL .= " union ";
1221                                 $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id,'us' as country_code,";
1222                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1223                                 $sSQL .= "null as placename,";
1224                                 $sSQL .= "null as ref,";
1225                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1226                                 //$sSQL .= $sOrderSQL." as porder, ";
1227                                 $sSQL .= "-0.10 as importance, ";
1228                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
1229                                 $sSQL .= "null as extra_place ";
1230                                 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
1231                                 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1232                                 $sSQL .= "group by place_id";
1233                                 if (!$bDeDupe) $sSQL .= ",place_id";
1234                                 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1235                                 $sSQL .= "order by importance desc";
1236                                 //$sSQL .= "order by rank_search,rank_address,porder asc";
1237                                 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
1238                                 $aSearchResults = $oDB->getAll($sSQL);
1239                                 //var_dump($sSQL,$aSearchResults);exit;
1240
1241                                 if (PEAR::IsError($aSearchResults))
1242                                 {
1243                                         failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
1244                                 }
1245                         }
1246                 } // end if ($sQuery)
1247                 else
1248                 {
1249                         if (isset($_GET['nearlat']) && trim($_GET['nearlat'])!=='' && isset($_GET['nearlon']) && trim($_GET['nearlon']) !== '')
1250                         {
1251                                 $iPlaceID = geocodeReverse((float)$_GET['nearlat'], (float)$_GET['nearlon']);
1252
1253                                 if ($iPlaceID)
1254                                 {
1255                                         $aResultPlaceIDs = array($iPlaceID);
1256                                         // TODO: this needs refactoring!
1257
1258                                         // Get the details for display (is this a redundant extra step?)
1259                                         $sPlaceIDs = join(',',$aResultPlaceIDs);
1260                                         $sOrderSQL = 'CASE ';
1261                                         foreach(array_keys($aResultPlaceIDs) as $iOrder => $iPlaceID)
1262                                         {
1263                                                 $sOrderSQL .= 'when min(place_id) = '.$iPlaceID.' then '.$iOrder.' ';
1264                                         }
1265                                         $sOrderSQL .= ' ELSE 10000000 END';
1266                                         $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id,calculated_country_code as country_code,";
1267                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1268                                         $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
1269                                         $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
1270                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1271                                         //$sSQL .= $sOrderSQL." as porder, ";
1272                                         $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
1273                                         $sSQL .= "(extratags->'place') as extra_place ";
1274                                         $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
1275                                         $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
1276                                         if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1277                                         if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1278                                         $sSQL .= ") ";
1279                                         $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
1280                                         if (!$bDeDupe) $sSQL .= ",place_id";
1281                                         $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1282                                         $sSQL .= ",get_name_by_language(name, $sLanguagePrefArraySQL) ";
1283                                         $sSQL .= ",get_name_by_language(name, ARRAY['ref']) ";
1284                                         $sSQL .= ",extratags->'place' ";
1285                                         $sSQL .= " union ";
1286                                         $sSQL .= "select 'T' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id,'us' as country_code,";
1287                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1288                                         $sSQL .= "null as placename,";
1289                                         $sSQL .= "null as ref,";
1290                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1291                                         //$sSQL .= $sOrderSQL." as porder, ";
1292                                         $sSQL .= "-0.15 as importance, ";
1293                                         $sSQL .= "null as extra_place ";
1294                                         $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
1295                                         $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1296                                         $sSQL .= "group by place_id";
1297                                         if (!$bDeDupe) $sSQL .= ",place_id";
1298                                         $sSQL .= " union ";
1299                                         $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id,'us' as country_code,";
1300                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1301                                         $sSQL .= "null as placename,";
1302                                         $sSQL .= "null as ref,";
1303                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1304                                         //$sSQL .= $sOrderSQL." as porder, ";
1305                                         $sSQL .= "-0.10 as importance, ";
1306                                         $sSQL .= "null as extra_place ";
1307                                         $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
1308                                         $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1309                                         $sSQL .= "group by place_id";
1310                                         if (!$bDeDupe) $sSQL .= ",place_id";
1311                                         $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1312                                         $sSQL .= "order by importance desc";
1313                                         //$sSQL .= "order by rank_search,rank_address,porder asc";
1314                                         if (CONST_Debug) { echo "<hr>", var_dump($sSQL); }
1315                                         $aSearchResults = $oDB->getAll($sSQL);
1316                                         //var_dump($sSQL,$aSearchResults);exit;
1317
1318                                         if (PEAR::IsError($aSearchResults))
1319                                         {
1320                                                 failInternalError("Could not get details for place (near).", $sSQL, $aSearchResults);
1321                                         }
1322                                 }
1323                                 else
1324                                 {
1325                                         $aSearchResults = array();
1326                                 }
1327                         }
1328                 }
1329         }
1330
1331         $sSearchResult = '';
1332         if (!sizeof($aSearchResults) && isset($_GET['q']) && $_GET['q'])
1333         {
1334                 $sSearchResult = 'No Results Found';
1335         }
1336         //var_Dump($aSearchResults);
1337         //exit;
1338         $aClassType = getClassTypesWithImportance();
1339         $aRecheckWords = preg_split('/\b/u',$sQuery);
1340         foreach($aRecheckWords as $i => $sWord)
1341         {
1342                 if (!$sWord) unset($aRecheckWords[$i]);
1343         }
1344         foreach($aSearchResults as $iResNum => $aResult)
1345         {
1346                 if (CONST_Search_AreaPolygons)
1347                 {
1348                         // Get the bounding box and outline polygon
1349                         $sSQL = "select place_id,numfeatures,area,outline,";
1350                         $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(outline)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(outline)),2)) as maxlat,";
1351                         $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(outline)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(outline)),3)) as maxlon,";
1352                         $sSQL .= "ST_AsText(outline) as outlinestring from get_place_boundingbox_quick(".$aResult['place_id'].")";
1353
1354                         $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1355                         $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1356                         $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1357                         $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1358                         if ($bAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1359                         if ($bAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1360                         if ($bAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1361                         if ($bAsText || $bShowPolygons) $sSQL .= ",ST_AsText(geometry) as astext";
1362                         $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1363                         $aPointPolygon = $oDB->getRow($sSQL);
1364                         if (PEAR::IsError($aPointPolygon))
1365                         {
1366                                 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1367                         }
1368                         if ($aPointPolygon['place_id'])
1369                         {
1370                                 if ($bAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1371                                 if ($bAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1372                                 if ($bAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1373                                 if ($bAsText) $aResult['astext'] = $aPointPolygon['astext'];
1374
1375                                 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1376                                 {
1377                                         $aResult['lat'] = $aPointPolygon['centrelat'];
1378                                         $aResult['lon'] = $aPointPolygon['centrelon'];
1379                                 }
1380                                 if ($bShowPolygons)
1381                                 {
1382                                         // Translate geometary string to point array
1383                                         if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1384                                         {
1385                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1386                                         }
1387                                         elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1388                                         {
1389                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1390                                         }
1391                                         elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1392                                         {
1393                                                 $fRadius = 0.01;
1394                                                 $iSteps = ($fRadius * 40000)^2;
1395                                                 $fStepSize = (2*pi())/$iSteps;
1396                                                 $aPolyPoints = array();
1397                                                 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1398                                                 {
1399                                                         $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1400                                                 }
1401                                                 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1402                                                 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1403                                                 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1404                                                 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1405                                         }
1406                                 }
1407
1408                                 // Output data suitable for display (points and a bounding box)
1409                                 if ($bShowPolygons && isset($aPolyPoints))
1410                                 {
1411                                         $aResult['aPolyPoints'] = array();
1412                                         foreach($aPolyPoints as $aPoint)
1413                                         {
1414                                                 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1415                                         }
1416                                 }
1417                                 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1418                         }
1419                 }
1420
1421                 if ($aResult['extra_place'] == 'city')
1422                 {
1423                         $aResult['class'] = 'place';
1424                         $aResult['type'] = 'city';
1425                         $aResult['rank_search'] = 16;
1426                 }
1427
1428                 if (!isset($aResult['aBoundingBox']))
1429                 {
1430                         // Default
1431                         $fDiameter = 0.0001;
1432
1433                         if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1434                                         && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1435                         {
1436                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1437                         }
1438                         elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1439                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1440                         {
1441                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1442                         }
1443                         $fRadius = $fDiameter / 2;
1444
1445                         $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1446                         $fStepSize = (2*pi())/$iSteps;
1447                         $aPolyPoints = array();
1448                         for($f = 0; $f < 2*pi(); $f += $fStepSize)
1449                         {
1450                                 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1451                         }
1452                         $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1453                         $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1454                         $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1455                         $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1456
1457                         // Output data suitable for display (points and a bounding box)
1458                         if ($bShowPolygons)
1459                         {
1460                                 $aResult['aPolyPoints'] = array();
1461                                 foreach($aPolyPoints as $aPoint)
1462                                 {
1463                                         $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1464                                 }
1465                         }
1466                         $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1467                 }
1468
1469                 // Is there an icon set for this type of result?
1470                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1471                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1472                 {
1473                         $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1474                 }
1475
1476                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1477                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1478                 {
1479                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1480                 }
1481
1482                 if ($bShowAddressDetails)
1483                 {
1484                         $aResult['address'] = getAddressDetails($oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1485                         if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1486                         {
1487                                 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1488                         }
1489
1490                         //var_dump($aResult['address']);
1491                         //exit;
1492                 }
1493
1494                 // Adjust importance for the number of exact string matches in the result
1495                 $aResult['importance'] = max(0.001,$aResult['importance']);
1496                 $iCountWords = 0;
1497                 $sAddress = $aResult['langaddress'];
1498                 foreach($aRecheckWords as $i => $sWord)
1499                 {
1500                         if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1501                 }
1502
1503                 $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
1504
1505                 //if (CONST_Debug) var_dump($aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']);
1506                 /*
1507                    if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'])
1508                    && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'])
1509                    {
1510                    $aResult['importance'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'];
1511                    }
1512                    elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1513                    && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1514                    {
1515                    $aResult['importance'] = $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1516                    }
1517                    else
1518                    {
1519                    $aResult['importance'] = 1000000000000000;
1520                    }
1521                  */
1522                 $aResult['name'] = $aResult['langaddress'];
1523                 $aResult['foundorder'] = -$aResult['addressimportance'];
1524                 $aSearchResults[$iResNum] = $aResult;
1525         }
1526         uasort($aSearchResults, 'byImportance');
1527
1528         $aOSMIDDone = array();
1529         $aClassTypeNameDone = array();
1530         $aToFilter = $aSearchResults;
1531         $aSearchResults = array();
1532
1533         $bFirst = true;
1534         foreach($aToFilter as $iResNum => $aResult)
1535         {
1536                 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1537                 $aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1538                 if ($bFirst)
1539                 {
1540                         $fLat = $aResult['lat'];
1541                         $fLon = $aResult['lon'];
1542                         if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1543                         $bFirst = false;
1544                 }
1545                 if (!$bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1546                                         && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1547                 {
1548                         $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1549                         $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1550                         $aSearchResults[] = $aResult;
1551                 }
1552
1553                 // Absolute limit on number of results
1554                 if (sizeof($aSearchResults) >= $iFinalLimit) break;
1555         }
1556
1557         $sDataDate = $oDB->getOne("select TO_CHAR(lastimportdate - '2 minutes'::interval,'YYYY/MM/DD HH24:MI')||' GMT' from import_status limit 1");
1558
1559         if (isset($_GET['nearlat']) && isset($_GET['nearlon']))
1560         {
1561                 $sQuery .= ' ['.$_GET['nearlat'].','.$_GET['nearlon'].']';
1562         }
1563
1564         if ($sQuery)
1565         {
1566                 logEnd($oDB, $hLog, sizeof($aToFilter));
1567         }
1568         $sMoreURL = CONST_Website_BaseURL.'search?format='.urlencode($sOutputFormat).'&exclude_place_ids='.join(',',$aExcludePlaceIDs);
1569         if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) $sMoreURL .= '&accept-language='.$_SERVER["HTTP_ACCEPT_LANGUAGE"];
1570         if ($bShowPolygons) $sMoreURL .= '&polygon=1';
1571         if ($bShowAddressDetails) $sMoreURL .= '&addressdetails=1';
1572         if (isset($_GET['viewbox']) && $_GET['viewbox']) $sMoreURL .= '&viewbox='.urlencode($_GET['viewbox']);
1573         if (isset($_GET['nearlat']) && isset($_GET['nearlon'])) $sMoreURL .= '&nearlat='.(float)$_GET['nearlat'].'&nearlon='.(float)$_GET['nearlon'];
1574         $sMoreURL .= '&q='.urlencode($sQuery);
1575
1576         if (CONST_Debug) exit;
1577
1578         include(CONST_BasePath.'/lib/template/search-'.$sOutputFormat.'.php');