]> git.openstreetmap.org Git - nominatim.git/blob - website/search.php
Try alternative orderings of structured queries that include postal codes
[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                                                         $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
935
936                                                 if (sizeof($aTerms))
937                                                 {
938                                                         $sSQL = "select place_id";
939                                                         $sSQL .= " from search_name";
940                                                         $sSQL .= " where ".join(' and ',$aTerms);
941                                                         $sSQL .= " order by ".join(', ',$aOrder);
942                                                         if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
943                                                                 $sSQL .= " limit 50";
944                                                         elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
945                                                                 $sSQL .= " limit 1";
946                                                         else
947                                                                 $sSQL .= " limit ".$iLimit;
948
949                                                         if (CONST_Debug) { var_dump($sSQL); }
950                                                         $aViewBoxPlaceIDs = $oDB->getAll($sSQL);
951                                                         if (PEAR::IsError($aViewBoxPlaceIDs))
952                                                         {
953                                                                 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
954                                                         }
955                                                         //var_dump($aViewBoxPlaceIDs);
956                                                         // Did we have an viewbox matches?
957                                                         $aPlaceIDs = array();
958                                                         $bViewBoxMatch = false;
959                                                         foreach($aViewBoxPlaceIDs as $aViewBoxRow)
960                                                         {
961                                                                 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
962                                                                 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
963                                                                 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
964                                                                 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
965                                                                 $aPlaceIDs[] = $aViewBoxRow['place_id'];
966                                                         }
967                                                 }
968                                                 //var_Dump($aPlaceIDs);
969                                                 //exit;
970
971                                                 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
972                                                 {
973                                                         $aRoadPlaceIDs = $aPlaceIDs;
974                                                         $sPlaceIDs = join(',',$aPlaceIDs);
975
976                                                         // Now they are indexed look for a house attached to a street we found
977                                                         $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
978                                                         $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
979                                                         if (sizeof($aExcludePlaceIDs))
980                                                         {
981                                                                 $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
982                                                         }
983                                                         $sSQL .= " limit $iLimit";
984                                                         if (CONST_Debug) var_dump($sSQL);
985                                                         $aPlaceIDs = $oDB->getCol($sSQL);
986
987                                                         // If not try the aux fallback table
988                                                         if (!sizeof($aPlaceIDs))
989                                                         {
990                                                                 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
991                                                                 if (sizeof($aExcludePlaceIDs))
992                                                                 {
993                                                                         $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
994                                                                 }
995                                                                 //$sSQL .= " limit $iLimit";
996                                                                 if (CONST_Debug) var_dump($sSQL);
997                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
998                                                         }
999
1000                                                         if (!sizeof($aPlaceIDs))
1001                                                         {
1002                                                                 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1003                                                                 if (sizeof($aExcludePlaceIDs))
1004                                                                 {
1005                                                                         $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
1006                                                                 }
1007                                                                 //$sSQL .= " limit $iLimit";
1008                                                                 if (CONST_Debug) var_dump($sSQL);
1009                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
1010                                                         }
1011
1012                                                         // Fallback to the road
1013                                                         if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1014                                                         {
1015                                                                 $aPlaceIDs = $aRoadPlaceIDs;
1016                                                         }
1017
1018                                                 }
1019
1020                                                 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1021                                                 {
1022                                                         $sPlaceIDs = join(',',$aPlaceIDs);
1023                                                         $aClassPlaceIDs = array();
1024
1025                                                         if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1026                                                         {
1027                                                                 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1028                                                                 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1029                                                                 $sSQL .= " and linked_place_id is null";
1030                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1031                                                                 $sSQL .= " order by rank_search asc limit $iLimit";
1032                                                                 if (CONST_Debug) var_dump($sSQL);
1033                                                                 $aClassPlaceIDs = $oDB->getCol($sSQL);
1034                                                         }
1035
1036                                                         if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1037                                                         {
1038                                                                 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1039                                                                 $bCacheTable = $oDB->getOne($sSQL);
1040
1041                                                                 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1042
1043                                                                 if (CONST_Debug) var_dump($sSQL);
1044                                                                 $iMaxRank = ((int)$oDB->getOne($sSQL));
1045
1046                                                                 // For state / country level searches the normal radius search doesn't work very well
1047                                                                 $sPlaceGeom = false;
1048                                                                 if ($iMaxRank < 9 && $bCacheTable)
1049                                                                 {
1050                                                                         // Try and get a polygon to search in instead
1051                                                                         $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";
1052                                                                         if (CONST_Debug) var_dump($sSQL);
1053                                                                         $sPlaceGeom = $oDB->getOne($sSQL);
1054                                                                 }
1055
1056                                                                 if ($sPlaceGeom)
1057                                                                 {
1058                                                                         $sPlaceIDs = false;
1059                                                                 }
1060                                                                 else
1061                                                                 {
1062                                                                         $iMaxRank += 5;
1063                                                                         $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
1064                                                                         if (CONST_Debug) var_dump($sSQL);
1065                                                                         $aPlaceIDs = $oDB->getCol($sSQL);
1066                                                                         $sPlaceIDs = join(',',$aPlaceIDs);
1067                                                                 }
1068
1069                                                                 if ($sPlaceIDs || $sPlaceGeom)
1070                                                                 {
1071
1072                                                                         $fRange = 0.01;
1073                                                                         if ($bCacheTable)
1074                                                                         {
1075                                                                                 // More efficient - can make the range bigger
1076                                                                                 $fRange = 0.05;
1077
1078                                                                                 $sOrderBySQL = '';
1079                                                                                 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1080                                                                                 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1081                                                                                 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1082
1083                                                                                 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1084                                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1085                                                                                 if ($sPlaceIDs)
1086                                                                                 {
1087                                                                                         $sSQL .= ",placex as f where ";
1088                                                                                         $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1089                                                                                 }
1090                                                                                 if ($sPlaceGeom)
1091                                                                                 {
1092                                                                                         $sSQL .= " where ";
1093                                                                                         $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1094                                                                                 }
1095                                                                                 if (sizeof($aExcludePlaceIDs))
1096                                                                                 {
1097                                                                                         $sSQL .= " and l.place_id not in (".join(',',$aExcludePlaceIDs).")";
1098                                                                                 }
1099                                                                                 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1100                                                                                 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1101                                                                                 if ($iOffset) $sSQL .= " offset $iOffset";
1102                                                                                 $sSQL .= " limit $iLimit";
1103                                                                                 if (CONST_Debug) var_dump($sSQL);
1104                                                                                 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $oDB->getCol($sSQL));
1105                                                                         }
1106                                                                         else
1107                                                                         {
1108                                                                                 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1109
1110                                                                                 $sOrderBySQL = '';
1111                                                                                 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1112                                                                                 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1113
1114                                                                                 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1115                                                                                 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1116                                                                                 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1117                                                                                 if (sizeof($aExcludePlaceIDs))
1118                                                                                 {
1119                                                                                         $sSQL .= " and l.place_id not in (".join(',',$aExcludePlaceIDs).")";
1120                                                                                 }
1121                                                                                 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1122                                                                                 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1123                                                                                 if ($iOffset) $sSQL .= " offset $iOffset";
1124                                                                                 $sSQL .= " limit $iLimit";
1125                                                                                 if (CONST_Debug) var_dump($sSQL);
1126                                                                                 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $oDB->getCol($sSQL));
1127                                                                         }
1128                                                                 }
1129                                                         }
1130
1131                                                         $aPlaceIDs = $aClassPlaceIDs;
1132
1133                                                 }
1134
1135                                         }
1136
1137                                         if (PEAR::IsError($aPlaceIDs))
1138                                         {
1139                                                 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1140                                         }
1141
1142                                         if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1143
1144                                         foreach($aPlaceIDs as $iPlaceID)
1145                                         {
1146                                                 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1147                                         }
1148                                         if ($iQueryLoop > 20) break;
1149                                 }
1150
1151                                 //exit;
1152                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1153                                 if ($iGroupLoop > 4) break;
1154                                 if ($iQueryLoop > 30) break;
1155                         }
1156
1157                         // Did we find anything?
1158                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1159                         {
1160                                 //var_Dump($aResultPlaceIDs);exit;
1161                                 // Get the details for display (is this a redundant extra step?)
1162                                 $sPlaceIDs = join(',',$aResultPlaceIDs);
1163                                 $sOrderSQL = 'CASE ';
1164                                 foreach(array_keys($aResultPlaceIDs) as $iOrder => $iPlaceID)
1165                                 {
1166                                         $sOrderSQL .= 'when min(place_id) = '.$iPlaceID.' then '.$iOrder.' ';
1167                                 }
1168                                 $sOrderSQL .= ' ELSE 10000000 END';
1169                                 $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,";
1170                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1171                                 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
1172                                 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
1173                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1174                                 //$sSQL .= $sOrderSQL." as porder, ";
1175                                 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
1176                                 $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, ";
1177                                 $sSQL .= "(extratags->'place') as extra_place ";
1178                                 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
1179                                 $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
1180                                 if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1181                                 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1182                                 $sSQL .= ") ";
1183                                 if ($sAllowedTypesSQLList) $sSQL .= "and placex.class in $sAllowedTypesSQLList ";
1184                                 $sSQL .= "and linked_place_id is null ";
1185                                 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
1186                                 if (!$bDeDupe) $sSQL .= ",place_id";
1187                                 $sSQL .= ",langaddress ";
1188                                 $sSQL .= ",placename ";
1189                                 $sSQL .= ",ref ";
1190                                 $sSQL .= ",extratags->'place' ";
1191                                 $sSQL .= " union ";
1192                                 $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,";
1193                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1194                                 $sSQL .= "null as placename,";
1195                                 $sSQL .= "null as ref,";
1196                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1197                                 //$sSQL .= $sOrderSQL." as porder, ";
1198                                 $sSQL .= "-0.15 as importance, ";
1199                                 $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, ";
1200                                 $sSQL .= "null as extra_place ";
1201                                 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
1202                                 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1203                                 $sSQL .= "group by place_id";
1204                                 if (!$bDeDupe) $sSQL .= ",place_id";
1205                                 $sSQL .= " union ";
1206                                 $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,";
1207                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1208                                 $sSQL .= "null as placename,";
1209                                 $sSQL .= "null as ref,";
1210                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1211                                 //$sSQL .= $sOrderSQL." as porder, ";
1212                                 $sSQL .= "-0.10 as importance, ";
1213                                 $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, ";
1214                                 $sSQL .= "null as extra_place ";
1215                                 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
1216                                 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1217                                 $sSQL .= "group by place_id";
1218                                 if (!$bDeDupe) $sSQL .= ",place_id";
1219                                 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1220                                 $sSQL .= "order by importance desc";
1221                                 //$sSQL .= "order by rank_search,rank_address,porder asc";
1222                                 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
1223                                 $aSearchResults = $oDB->getAll($sSQL);
1224                                 //var_dump($sSQL,$aSearchResults);exit;
1225
1226                                 if (PEAR::IsError($aSearchResults))
1227                                 {
1228                                         failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
1229                                 }
1230                         }
1231                 } // end if ($sQuery)
1232                 else
1233                 {
1234                         if (isset($_GET['nearlat']) && trim($_GET['nearlat'])!=='' && isset($_GET['nearlon']) && trim($_GET['nearlon']) !== '')
1235                         {
1236                                 $iPlaceID = geocodeReverse((float)$_GET['nearlat'], (float)$_GET['nearlon']);
1237
1238                                 if ($iPlaceID)
1239                                 {
1240                                         $aResultPlaceIDs = array($iPlaceID);
1241                                         // TODO: this needs refactoring!
1242
1243                                         // Get the details for display (is this a redundant extra step?)
1244                                         $sPlaceIDs = join(',',$aResultPlaceIDs);
1245                                         $sOrderSQL = 'CASE ';
1246                                         foreach(array_keys($aResultPlaceIDs) as $iOrder => $iPlaceID)
1247                                         {
1248                                                 $sOrderSQL .= 'when min(place_id) = '.$iPlaceID.' then '.$iOrder.' ';
1249                                         }
1250                                         $sOrderSQL .= ' ELSE 10000000 END';
1251                                         $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,";
1252                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1253                                         $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
1254                                         $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
1255                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1256                                         //$sSQL .= $sOrderSQL." as porder, ";
1257                                         $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
1258                                         $sSQL .= "(extratags->'place') as extra_place ";
1259                                         $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
1260                                         $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
1261                                         if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1262                                         if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1263                                         $sSQL .= ") ";
1264                                         $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
1265                                         if (!$bDeDupe) $sSQL .= ",place_id";
1266                                         $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1267                                         $sSQL .= ",get_name_by_language(name, $sLanguagePrefArraySQL) ";
1268                                         $sSQL .= ",get_name_by_language(name, ARRAY['ref']) ";
1269                                         $sSQL .= ",extratags->'place' ";
1270                                         $sSQL .= " union ";
1271                                         $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,";
1272                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1273                                         $sSQL .= "null as placename,";
1274                                         $sSQL .= "null as ref,";
1275                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1276                                         //$sSQL .= $sOrderSQL." as porder, ";
1277                                         $sSQL .= "-0.15 as importance, ";
1278                                         $sSQL .= "null as extra_place ";
1279                                         $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
1280                                         $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1281                                         $sSQL .= "group by place_id";
1282                                         if (!$bDeDupe) $sSQL .= ",place_id";
1283                                         $sSQL .= " union ";
1284                                         $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,";
1285                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1286                                         $sSQL .= "null as placename,";
1287                                         $sSQL .= "null as ref,";
1288                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1289                                         //$sSQL .= $sOrderSQL." as porder, ";
1290                                         $sSQL .= "-0.10 as importance, ";
1291                                         $sSQL .= "null as extra_place ";
1292                                         $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
1293                                         $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1294                                         $sSQL .= "group by place_id";
1295                                         if (!$bDeDupe) $sSQL .= ",place_id";
1296                                         $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1297                                         $sSQL .= "order by importance desc";
1298                                         //$sSQL .= "order by rank_search,rank_address,porder asc";
1299                                         if (CONST_Debug) { echo "<hr>", var_dump($sSQL); }
1300                                         $aSearchResults = $oDB->getAll($sSQL);
1301                                         //var_dump($sSQL,$aSearchResults);exit;
1302
1303                                         if (PEAR::IsError($aSearchResults))
1304                                         {
1305                                                 failInternalError("Could not get details for place (near).", $sSQL, $aSearchResults);
1306                                         }
1307                                 }
1308                                 else
1309                                 {
1310                                         $aSearchResults = array();
1311                                 }
1312                         }
1313                 }
1314         }
1315
1316         $sSearchResult = '';
1317         if (!sizeof($aSearchResults) && isset($_GET['q']) && $_GET['q'])
1318         {
1319                 $sSearchResult = 'No Results Found';
1320         }
1321         //var_Dump($aSearchResults);
1322         //exit;
1323         $aClassType = getClassTypesWithImportance();
1324         $aRecheckWords = preg_split('/\b/u',$sQuery);
1325         foreach($aRecheckWords as $i => $sWord)
1326         {
1327                 if (!$sWord) unset($aRecheckWords[$i]);
1328         }
1329         foreach($aSearchResults as $iResNum => $aResult)
1330         {
1331                 if (CONST_Search_AreaPolygons)
1332                 {
1333                         // Get the bounding box and outline polygon
1334                         $sSQL = "select place_id,numfeatures,area,outline,";
1335                         $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(outline)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(outline)),2)) as maxlat,";
1336                         $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(outline)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(outline)),3)) as maxlon,";
1337                         $sSQL .= "ST_AsText(outline) as outlinestring from get_place_boundingbox_quick(".$aResult['place_id'].")";
1338
1339                         $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1340                         $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1341                         $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1342                         $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1343                         if ($bAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1344                         if ($bAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1345                         if ($bAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1346                         if ($bAsText || $bShowPolygons) $sSQL .= ",ST_AsText(geometry) as astext";
1347                         $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1348                         $aPointPolygon = $oDB->getRow($sSQL);
1349                         if (PEAR::IsError($aPointPolygon))
1350                         {
1351                                 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1352                         }
1353                         if ($aPointPolygon['place_id'])
1354                         {
1355                                 if ($bAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1356                                 if ($bAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1357                                 if ($bAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1358                                 if ($bAsText) $aResult['astext'] = $aPointPolygon['astext'];
1359
1360                                 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1361                                 {
1362                                         $aResult['lat'] = $aPointPolygon['centrelat'];
1363                                         $aResult['lon'] = $aPointPolygon['centrelon'];
1364                                 }
1365                                 if ($bShowPolygons)
1366                                 {
1367                                         // Translate geometary string to point array
1368                                         if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1369                                         {
1370                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1371                                         }
1372                                         elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1373                                         {
1374                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1375                                         }
1376                                         elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1377                                         {
1378                                                 $fRadius = 0.01;
1379                                                 $iSteps = ($fRadius * 40000)^2;
1380                                                 $fStepSize = (2*pi())/$iSteps;
1381                                                 $aPolyPoints = array();
1382                                                 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1383                                                 {
1384                                                         $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1385                                                 }
1386                                                 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1387                                                 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1388                                                 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1389                                                 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1390                                         }
1391                                 }
1392
1393                                 // Output data suitable for display (points and a bounding box)
1394                                 if ($bShowPolygons && isset($aPolyPoints))
1395                                 {
1396                                         $aResult['aPolyPoints'] = array();
1397                                         foreach($aPolyPoints as $aPoint)
1398                                         {
1399                                                 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1400                                         }
1401                                 }
1402                                 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1403                         }
1404                 }
1405
1406                 if ($aResult['extra_place'] == 'city')
1407                 {
1408                         $aResult['class'] = 'place';
1409                         $aResult['type'] = 'city';
1410                         $aResult['rank_search'] = 16;
1411                 }
1412
1413                 if (!isset($aResult['aBoundingBox']))
1414                 {
1415                         // Default
1416                         $fDiameter = 0.0001;
1417
1418                         if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1419                                         && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1420                         {
1421                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1422                         }
1423                         elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1424                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1425                         {
1426                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1427                         }
1428                         $fRadius = $fDiameter / 2;
1429
1430                         $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1431                         $fStepSize = (2*pi())/$iSteps;
1432                         $aPolyPoints = array();
1433                         for($f = 0; $f < 2*pi(); $f += $fStepSize)
1434                         {
1435                                 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1436                         }
1437                         $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1438                         $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1439                         $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1440                         $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1441
1442                         // Output data suitable for display (points and a bounding box)
1443                         if ($bShowPolygons)
1444                         {
1445                                 $aResult['aPolyPoints'] = array();
1446                                 foreach($aPolyPoints as $aPoint)
1447                                 {
1448                                         $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1449                                 }
1450                         }
1451                         $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1452                 }
1453
1454                 // Is there an icon set for this type of result?
1455                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1456                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1457                 {
1458                         $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1459                 }
1460
1461                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1462                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1463                 {
1464                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1465                 }
1466
1467                 if ($bShowAddressDetails)
1468                 {
1469                         $aResult['address'] = getAddressDetails($oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1470                         if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1471                         {
1472                                 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1473                         }
1474
1475                         //var_dump($aResult['address']);
1476                         //exit;
1477                 }
1478
1479                 // Adjust importance for the number of exact string matches in the result
1480                 $aResult['importance'] = max(0.001,$aResult['importance']);
1481                 $iCountWords = 0;
1482                 $sAddress = $aResult['langaddress'];
1483                 foreach($aRecheckWords as $i => $sWord)
1484                 {
1485                         if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1486                 }
1487
1488                 $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
1489
1490                 //if (CONST_Debug) var_dump($aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']);
1491                 /*
1492                    if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'])
1493                    && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'])
1494                    {
1495                    $aResult['importance'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'];
1496                    }
1497                    elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1498                    && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1499                    {
1500                    $aResult['importance'] = $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1501                    }
1502                    else
1503                    {
1504                    $aResult['importance'] = 1000000000000000;
1505                    }
1506                  */
1507                 $aResult['name'] = $aResult['langaddress'];
1508                 $aResult['foundorder'] = -$aResult['addressimportance'];
1509                 $aSearchResults[$iResNum] = $aResult;
1510         }
1511         uasort($aSearchResults, 'byImportance');
1512
1513         $aOSMIDDone = array();
1514         $aClassTypeNameDone = array();
1515         $aToFilter = $aSearchResults;
1516         $aSearchResults = array();
1517
1518         $bFirst = true;
1519         foreach($aToFilter as $iResNum => $aResult)
1520         {
1521                 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1522                 $aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1523                 if ($bFirst)
1524                 {
1525                         $fLat = $aResult['lat'];
1526                         $fLon = $aResult['lon'];
1527                         if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1528                         $bFirst = false;
1529                 }
1530                 if (!$bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1531                                         && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1532                 {
1533                         $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1534                         $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1535                         $aSearchResults[] = $aResult;
1536                 }
1537
1538                 // Absolute limit on number of results
1539                 if (sizeof($aSearchResults) >= $iFinalLimit) break;
1540         }
1541
1542         $sDataDate = $oDB->getOne("select TO_CHAR(lastimportdate - '2 minutes'::interval,'YYYY/MM/DD HH24:MI')||' GMT' from import_status limit 1");
1543
1544         if (isset($_GET['nearlat']) && isset($_GET['nearlon']))
1545         {
1546                 $sQuery .= ' ['.$_GET['nearlat'].','.$_GET['nearlon'].']';
1547         }
1548
1549         if ($sQuery)
1550         {
1551                 logEnd($oDB, $hLog, sizeof($aToFilter));
1552         }
1553         $sMoreURL = CONST_Website_BaseURL.'search?format='.urlencode($sOutputFormat).'&exclude_place_ids='.join(',',$aExcludePlaceIDs);
1554         if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) $sMoreURL .= '&accept-language='.$_SERVER["HTTP_ACCEPT_LANGUAGE"];
1555         if ($bShowPolygons) $sMoreURL .= '&polygon=1';
1556         if ($bShowAddressDetails) $sMoreURL .= '&addressdetails=1';
1557         if (isset($_GET['viewbox']) && $_GET['viewbox']) $sMoreURL .= '&viewbox='.urlencode($_GET['viewbox']);
1558         if (isset($_GET['nearlat']) && isset($_GET['nearlon'])) $sMoreURL .= '&nearlat='.(float)$_GET['nearlat'].'&nearlon='.(float)$_GET['nearlon'];
1559         $sMoreURL .= '&q='.urlencode($sQuery);
1560
1561         if (CONST_Debug) exit;
1562
1563         include(CONST_BasePath.'/lib/template/search-'.$sOutputFormat.'.php');