]> git.openstreetmap.org Git - nominatim.git/blob - lib/setup/SetupClass.php
typo
[nominatim.git] / lib / setup / SetupClass.php
1 <?php
2
3 namespace Nominatim\Setup;
4
5 class SetupFunctions
6 {
7     protected $iCacheMemory;            // set in constructor
8     protected $iInstances;              // set in constructor
9     protected $sModulePath;             // set in constructor
10     protected $aDSNInfo;                // set in constructor = DB::parseDSN(CONST_Database_DSN);
11     protected $sVerbose;                // set in constructor
12     protected $sIgnoreErrors;           // set in constructor
13     protected $bEnableDiffUpdates;      // set in constructor
14     protected $bEnableDebugStatements;  // set in constructor
15     protected $bNoPartitions;           // set in constructor
16     protected $oDB = null;              // set in setupDB (earliest) or later in loadData, importData, drop, createSqlFunctions, importTigerData
17                                                 // pgsqlRunPartitionScript, calculatePostcodes, ..if no already set
18
19     public function __construct($aCMDResult, $callingFunction = 'setup')
20     {
21         // by default, use all but one processor, but never more than 15.
22         $this->iInstances = isset($aCMDResult['threads'])
23             ? $aCMDResult['threads']
24             : (min(16, getProcessorCount()) - 1);
25
26
27         if ($this->iInstances < 1) {
28             $this->iInstances = 1;
29             warn('resetting threads to '.$this->iInstances);
30         }
31
32         // Assume we can steal all the cache memory in the box (unless told otherwise)
33         if (isset($aCMDResult['osm2pgsql-cache'])) {
34             $this->iCacheMemory = $aCMDResult['osm2pgsql-cache'];
35         } else {
36             $this->iCacheMemory = getCacheMemoryMB();
37         }
38
39         $this->sModulePath = CONST_Database_Module_Path;
40         info('module path: ' . $this->sModulePath);
41        
42         // prepares DB for import or update, sets the Data Source Name
43         $this->aDSNInfo = \DB::parseDSN(CONST_Database_DSN);
44         if (!isset($this->aDSNInfo['port']) || !$this->aDSNInfo['port']) $this->aDSNInfo['port'] = 5432;
45  
46         // setting member variables based on command line options stored in $aCMDResult
47         $this->sVerbose = $aCMDResult['verbose'];
48
49         //setting default values which are not set by the update.php array
50         if (isset($aCMDResult['ignore-errors'])) {
51             $this->sIgnoreErrors = $aCMDResult['ignore-errors'];
52         } else {
53             $this->sIgnoreErrors = false;
54         }
55         if (isset($aCMDResult['enable-debug-statements'])) {
56             $this->bEnableDebugStatements = $aCMDResult['enable-debug-statements'];
57         } else {
58             $this->bEnableDebugStatements = false;
59         }
60         if (isset($aCMDResult['no-partitions'])) {
61             $this->bNoPartitions = $aCMDResult['no-partitions'];
62         } else {
63             $this->bNoPartitions = false;
64         }
65
66         // if class is instantiated by update.php, we have to set EnableDiffUpdates to true
67         // otherwise set to value provided by setup.php's command line arg array
68         if ($callingFunction == 'update') {
69             $this->bEnableDiffUpdates = true;
70         } elseif ($callingFunction == 'setup') {
71             $this->bEnableDiffUpdates = $aCMDResult['enable-diff-updates'];
72         }
73     }
74
75     public function createDB()
76     {
77         info('Create DB');
78         $sDB = \DB::connect(CONST_Database_DSN, false);
79         if (!\PEAR::isError($sDB)) {
80             fail('database already exists ('.CONST_Database_DSN.')');
81         }
82
83         $sCreateDBCmd = 'createdb -E UTF-8 -p '.$this->aDSNInfo['port'].' '.$this->aDSNInfo['database'];
84         if (isset($this->aDSNInfo['username']) && $this->aDSNInfo['username']) {
85             $sCreateDBCmd .= ' -U '.$this->aDSNInfo['username'];
86         }
87
88         if (isset($this->aDSNInfo['hostspec']) && $this->aDSNInfo['hostspec']) {
89             $sCreateDBCmd .= ' -h '.$this->aDSNInfo['hostspec'];
90         }
91
92         $aProcEnv = null;
93         if (isset($this->aDSNInfo['password']) && $this->aDSNInfo['password']) {
94             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
95         }
96
97         $result = runWithEnv($sCreateDBCmd, $aProcEnv);
98         if ($result != 0) fail('Error executing external command: '.$sCreateDBCmd);
99     }
100
101     public function setupDB()
102     {
103         info('Setup DB');
104         $this->oDB =& getDB();
105
106         $fPostgresVersion = getPostgresVersion($this->oDB);
107         echo 'Postgres version found: '.$fPostgresVersion."\n";
108
109         if ($fPostgresVersion < 9.1) {
110             fail('Minimum supported version of Postgresql is 9.1.');
111         }
112
113         $this->pgsqlRunScript('CREATE EXTENSION IF NOT EXISTS hstore');
114         $this->pgsqlRunScript('CREATE EXTENSION IF NOT EXISTS postgis');
115
116         // For extratags and namedetails the hstore_to_json converter is
117         // needed which is only available from Postgresql 9.3+. For older
118         // versions add a dummy function that returns nothing.
119         $iNumFunc = chksql($this->oDB->getOne("select count(*) from pg_proc where proname = 'hstore_to_json'"));
120
121         if ($iNumFunc == 0) {
122             $this->pgsqlRunScript("create function hstore_to_json(dummy hstore) returns text AS 'select null::text' language sql immutable");
123             warn('Postgresql is too old. extratags and namedetails API not available.');
124         }
125
126
127         $fPostgisVersion = getPostgisVersion($this->oDB);
128         echo 'Postgis version found: '.$fPostgisVersion."\n";
129
130         if ($fPostgisVersion < 2.1) {
131             // Functions were renamed in 2.1 and throw an annoying deprecation warning
132             $this->pgsqlRunScript('ALTER FUNCTION st_line_interpolate_point(geometry, double precision) RENAME TO ST_LineInterpolatePoint');
133             $this->pgsqlRunScript('ALTER FUNCTION ST_Line_Locate_Point(geometry, geometry) RENAME TO ST_LineLocatePoint');
134         }
135         if ($fPostgisVersion < 2.2) {
136             $this->pgsqlRunScript('ALTER FUNCTION ST_Distance_Spheroid(geometry, geometry, spheroid) RENAME TO ST_DistanceSpheroid');
137         }
138
139         $i = chksql($this->oDB->getOne("select count(*) from pg_user where usename = '".CONST_Database_Web_User."'"));
140         if ($i == 0) {
141             echo "\nERROR: Web user '".CONST_Database_Web_User."' does not exist. Create it with:\n";
142             echo "\n          createuser ".CONST_Database_Web_User."\n\n";
143             exit(1);
144         }
145
146         // Try accessing the C module, so we know early if something is wrong
147         if (!checkModulePresence()) {
148             fail('error loading nominatim.so module');
149         }
150
151         if (!file_exists(CONST_ExtraDataPath.'/country_osm_grid.sql.gz')) {
152             echo 'Error: you need to download the country_osm_grid first:';
153             echo "\n    wget -O ".CONST_ExtraDataPath."/country_osm_grid.sql.gz https://www.nominatim.org/data/country_grid.sql.gz\n";
154             exit(1);
155         }
156         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/country_name.sql');
157         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/country_naturalearthdata.sql');
158         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/country_osm_grid.sql.gz');
159         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/gb_postcode_table.sql');
160
161
162         if (file_exists(CONST_BasePath.'/data/gb_postcode_data.sql.gz')) {
163             $this->pgsqlRunScriptFile(CONST_BasePath.'/data/gb_postcode_data.sql.gz');
164         } else {
165             warn('external UK postcode table not found.');
166         }
167
168         if (CONST_Use_Extra_US_Postcodes) {
169             $this->pgsqlRunScriptFile(CONST_BasePath.'/data/us_postcode.sql');
170         }
171
172         if ($this->bNoPartitions) {
173             $this->pgsqlRunScript('update country_name set partition = 0');
174         }
175
176         // the following will be needed by createFunctions later but
177         // is only defined in the subsequently called createTables
178         // Create dummies here that will be overwritten by the proper
179         // versions in create-tables.
180         $this->pgsqlRunScript('CREATE TABLE IF NOT EXISTS place_boundingbox ()');
181         $this->pgsqlRunScript('CREATE TYPE wikipedia_article_match AS ()', false);
182     }
183
184     public function importData($sOSMFile)
185     {
186         info('Import data');
187
188         $osm2pgsql = CONST_Osm2pgsql_Binary;
189         if (!file_exists($osm2pgsql)) {
190             echo "Check CONST_Osm2pgsql_Binary in your local settings file.\n";
191             echo "Normally you should not need to set this manually.\n";
192             fail("osm2pgsql not found in '$osm2pgsql'");
193         }
194
195
196
197         if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
198             $osm2pgsql .= ' --flat-nodes '.CONST_Osm2pgsql_Flatnode_File;
199         }
200
201         if (CONST_Tablespace_Osm2pgsql_Data)
202             $osm2pgsql .= ' --tablespace-slim-data '.CONST_Tablespace_Osm2pgsql_Data;
203         if (CONST_Tablespace_Osm2pgsql_Index)
204             $osm2pgsql .= ' --tablespace-slim-index '.CONST_Tablespace_Osm2pgsql_Index;
205         if (CONST_Tablespace_Place_Data)
206             $osm2pgsql .= ' --tablespace-main-data '.CONST_Tablespace_Place_Data;
207         if (CONST_Tablespace_Place_Index)
208             $osm2pgsql .= ' --tablespace-main-index '.CONST_Tablespace_Place_Index;
209         $osm2pgsql .= ' -lsc -O gazetteer --hstore --number-processes 1';
210         $osm2pgsql .= ' -C '.$this->iCacheMemory;
211         $osm2pgsql .= ' -P '.$this->aDSNInfo['port'];
212         if (isset($this->aDSNInfo['username']) && $this->aDSNInfo['username']) {
213             $osm2pgsql .= ' -U '.$this->aDSNInfo['username'];
214         }
215         if (isset($this->aDSNInfo['hostspec']) && $this->aDSNInfo['hostspec']) {
216             $osm2pgsql .= ' -H '.$this->aDSNInfo['hostspec'];
217         }
218         $aProcEnv = null;
219         if (isset($this->aDSNInfo['password']) && $this->aDSNInfo['password']) {
220             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
221         }
222         $osm2pgsql .= ' -d '.$this->aDSNInfo['database'].' '.$sOSMFile;
223         runWithEnv($osm2pgsql, $aProcEnv);
224         if ($this->oDB == null) $this->oDB =& getDB();
225         if (!$this->sIgnoreErrors && !chksql($this->oDB->getRow('select * from place limit 1'))) {
226             fail('No Data');
227         }
228     }
229
230     public function createFunctions()
231     {
232         info('Create Functions');
233
234         // Try accessing the C module, so we know eif something is wrong
235         // update.php calls this function
236         if (!checkModulePresence()) {
237             fail('error loading nominatim.so module');
238         }
239         $this->createSqlFunctions();
240     }
241
242     public function createTables()
243     {
244         info('Create Tables');
245
246         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tables.sql');
247         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
248         $sTemplate = $this->replaceTablespace(
249             '{ts:address-data}',
250             CONST_Tablespace_Address_Data,
251             $sTemplate
252         );
253         $sTemplate = $this->replaceTablespace(
254             '{ts:address-index}',
255             CONST_Tablespace_Address_Index,
256             $sTemplate
257         );
258         $sTemplate = $this->replaceTablespace(
259             '{ts:search-data}',
260             CONST_Tablespace_Search_Data,
261             $sTemplate
262         );
263         $sTemplate = $this->replaceTablespace(
264             '{ts:search-index}',
265             CONST_Tablespace_Search_Index,
266             $sTemplate
267         );
268         $sTemplate = $this->replaceTablespace(
269             '{ts:aux-data}',
270             CONST_Tablespace_Aux_Data,
271             $sTemplate
272         );
273         $sTemplate = $this->replaceTablespace(
274             '{ts:aux-index}',
275             CONST_Tablespace_Aux_Index,
276             $sTemplate
277         );
278
279         $this->pgsqlRunScript($sTemplate, false);
280     }
281
282     public function createPartitionTables()
283     {
284         info('Create Partition Tables');
285
286         $sTemplate = file_get_contents(CONST_BasePath.'/sql/partition-tables.src.sql');
287         $sTemplate = $this->replaceTablespace(
288             '{ts:address-data}',
289             CONST_Tablespace_Address_Data,
290             $sTemplate
291         );
292
293         $sTemplate = $this->replaceTablespace(
294             '{ts:address-index}',
295             CONST_Tablespace_Address_Index,
296             $sTemplate
297         );
298
299         $sTemplate = $this->replaceTablespace(
300             '{ts:search-data}',
301             CONST_Tablespace_Search_Data,
302             $sTemplate
303         );
304
305         $sTemplate = $this->replaceTablespace(
306             '{ts:search-index}',
307             CONST_Tablespace_Search_Index,
308             $sTemplate
309         );
310
311         $sTemplate = $this->replaceTablespace(
312             '{ts:aux-data}',
313             CONST_Tablespace_Aux_Data,
314             $sTemplate
315         );
316
317         $sTemplate = $this->replaceTablespace(
318             '{ts:aux-index}',
319             CONST_Tablespace_Aux_Index,
320             $sTemplate
321         );
322
323         $this->pgsqlRunPartitionScript($sTemplate);
324     }
325
326     public function createPartitionFunctions()
327     {
328         info('Create Partition Functions');
329
330         $sTemplate = file_get_contents(CONST_BasePath.'/sql/partition-functions.src.sql');
331         $this->pgsqlRunPartitionScript($sTemplate);
332     }
333
334     public function importWikipediaArticles()
335     {
336         $sWikiArticlesFile = CONST_Wikipedia_Data_Path.'/wikipedia_article.sql.bin';
337         $sWikiRedirectsFile = CONST_Wikipedia_Data_Path.'/wikipedia_redirect.sql.bin';
338         if (file_exists($sWikiArticlesFile)) {
339             info('Importing wikipedia articles');
340             $this->pgsqlRunDropAndRestore($sWikiArticlesFile);
341         } else {
342             warn('wikipedia article dump file not found - places will have default importance');
343         }
344         if (file_exists($sWikiRedirectsFile)) {
345             info('Importing wikipedia redirects');
346             $this->pgsqlRunDropAndRestore($sWikiRedirectsFile);
347         } else {
348             warn('wikipedia redirect dump file not found - some place importance values may be missing');
349         }
350     }
351
352     public function loadData($bDisableTokenPrecalc)
353     {
354         info('Drop old Data');
355
356         if ($this->oDB == null) $this->oDB =& getDB();
357
358         if (!pg_query($this->oDB->connection, 'TRUNCATE word')) fail(pg_last_error($this->oDB->connection));
359         echo '.';
360         if (!pg_query($this->oDB->connection, 'TRUNCATE placex')) fail(pg_last_error($this->oDB->connection));
361         echo '.';
362         if (!pg_query($this->oDB->connection, 'TRUNCATE location_property_osmline')) fail(pg_last_error($this->oDB->connection));
363         echo '.';
364         if (!pg_query($this->oDB->connection, 'TRUNCATE place_addressline')) fail(pg_last_error($this->oDB->connection));
365         echo '.';
366         if (!pg_query($this->oDB->connection, 'TRUNCATE place_boundingbox')) fail(pg_last_error($this->oDB->connection));
367         echo '.';
368         if (!pg_query($this->oDB->connection, 'TRUNCATE location_area')) fail(pg_last_error($this->oDB->connection));
369         echo '.';
370         if (!pg_query($this->oDB->connection, 'TRUNCATE search_name')) fail(pg_last_error($this->oDB->connection));
371         echo '.';
372         if (!pg_query($this->oDB->connection, 'TRUNCATE search_name_blank')) fail(pg_last_error($this->oDB->connection));
373         echo '.';
374         if (!pg_query($this->oDB->connection, 'DROP SEQUENCE seq_place')) fail(pg_last_error($this->oDB->connection));
375         echo '.';
376         if (!pg_query($this->oDB->connection, 'CREATE SEQUENCE seq_place start 100000')) fail(pg_last_error($this->oDB->connection));
377         echo '.';
378
379         $sSQL = 'select distinct partition from country_name';
380         $aPartitions = chksql($this->oDB->getCol($sSQL));
381         if (!$this->bNoPartitions) $aPartitions[] = 0;
382         foreach ($aPartitions as $sPartition) {
383             if (!pg_query($this->oDB->connection, 'TRUNCATE location_road_'.$sPartition)) fail(pg_last_error($this->oDB->connection));
384             echo '.';
385         }
386
387         // used by getorcreate_word_id to ignore frequent partial words
388         $sSQL = 'CREATE OR REPLACE FUNCTION get_maxwordfreq() RETURNS integer AS ';
389         $sSQL .= '$$ SELECT '.CONST_Max_Word_Frequency.' as maxwordfreq; $$ LANGUAGE SQL IMMUTABLE';
390         if (!pg_query($this->oDB->connection, $sSQL)) {
391             fail(pg_last_error($this->oDB->connection));
392         }
393         echo ".\n";
394
395         // pre-create the word list
396         if (!$bDisableTokenPrecalc) {
397             info('Loading word list');
398             $this->pgsqlRunScriptFile(CONST_BasePath.'/data/words.sql');
399         }
400
401         info('Load Data');
402         $sColumns = 'osm_type, osm_id, class, type, name, admin_level, address, extratags, geometry';
403         $aDBInstances = array();
404         $iLoadThreads = max(1, $this->iInstances - 1);
405         for ($i = 0; $i < $iLoadThreads; $i++) {
406             $aDBInstances[$i] =& getDB(true);
407             $sSQL = "INSERT INTO placex ($sColumns) SELECT $sColumns FROM place WHERE osm_id % $iLoadThreads = $i";
408             $sSQL .= " and not (class='place' and type='houses' and osm_type='W'";
409             $sSQL .= "          and ST_GeometryType(geometry) = 'ST_LineString')";
410             $sSQL .= ' and ST_IsValid(geometry)';
411             if ($this->sVerbose) echo "$sSQL\n";
412             if (!pg_send_query($aDBInstances[$i]->connection, $sSQL)) {
413                 fail(pg_last_error($aDBInstances[$i]->connection));
414             }
415         }
416
417         // last thread for interpolation lines
418         $aDBInstances[$iLoadThreads] =& getDB(true);
419         $sSQL = 'insert into location_property_osmline';
420         $sSQL .= ' (osm_id, address, linegeo)';
421         $sSQL .= ' SELECT osm_id, address, geometry from place where ';
422         $sSQL .= "class='place' and type='houses' and osm_type='W' and ST_GeometryType(geometry) = 'ST_LineString'";
423         if ($this->sVerbose) echo "$sSQL\n";
424         if (!pg_send_query($aDBInstances[$iLoadThreads]->connection, $sSQL)) {
425             fail(pg_last_error($aDBInstances[$iLoadThreads]->connection));
426         }
427
428         $bFailed = false;
429         for ($i = 0; $i <= $iLoadThreads; $i++) {
430             while (($hPGresult = pg_get_result($aDBInstances[$i]->connection)) !== false) {
431                 $resultStatus = pg_result_status($hPGresult);
432                 // PGSQL_EMPTY_QUERY, PGSQL_COMMAND_OK, PGSQL_TUPLES_OK,
433                 // PGSQL_COPY_OUT, PGSQL_COPY_IN, PGSQL_BAD_RESPONSE,
434                 // PGSQL_NONFATAL_ERROR and PGSQL_FATAL_ERROR
435                 // echo 'Query result ' . $i . ' is: ' . $resultStatus . "\n";
436                 if ($resultStatus != PGSQL_COMMAND_OK && $resultStatus != PGSQL_TUPLES_OK) {
437                     $resultError = pg_result_error($hPGresult);
438                     echo '-- error text ' . $i . ': ' . $resultError . "\n";
439                     $bFailed = true;
440                 }
441             }
442         }
443         if ($bFailed) {
444             fail('SQL errors loading placex and/or location_property_osmline tables');
445         }
446         echo "\n";
447         info('Reanalysing database');
448         $this->pgsqlRunScript('ANALYSE');
449
450         $sDatabaseDate = getDatabaseDate($this->oDB);
451         pg_query($this->oDB->connection, 'TRUNCATE import_status');
452         if ($sDatabaseDate === false) {
453             warn('could not determine database date.');
454         } else {
455             $sSQL = "INSERT INTO import_status (lastimportdate) VALUES('".$sDatabaseDate."')";
456             pg_query($this->oDB->connection, $sSQL);
457             echo "Latest data imported from $sDatabaseDate.\n";
458         }
459     }
460
461     public function importTigerData()
462     {
463         info('Import Tiger data');
464
465         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tiger_import_start.sql');
466         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
467         $sTemplate = $this->replaceTablespace(
468             '{ts:aux-data}',
469             CONST_Tablespace_Aux_Data,
470             $sTemplate
471         );
472         $sTemplate = $this->replaceTablespace(
473             '{ts:aux-index}',
474             CONST_Tablespace_Aux_Index,
475             $sTemplate
476         );
477         $this->pgsqlRunScript($sTemplate, false);
478
479         $aDBInstances = array();
480         for ($i = 0; $i < $this->iInstances; $i++) {
481             $aDBInstances[$i] =& getDB(true);
482         }
483
484         foreach (glob(CONST_Tiger_Data_Path.'/*.sql') as $sFile) {
485             echo $sFile.': ';
486             $hFile = fopen($sFile, 'r');
487             $sSQL = fgets($hFile, 100000);
488             $iLines = 0;
489             while (true) {
490                 for ($i = 0; $i < $this->iInstances; $i++) {
491                     if (!pg_connection_busy($aDBInstances[$i]->connection)) {
492                         while (pg_get_result($aDBInstances[$i]->connection));
493                         $sSQL = fgets($hFile, 100000);
494                         if (!$sSQL) break 2;
495                         if (!pg_send_query($aDBInstances[$i]->connection, $sSQL)) fail(pg_last_error($this->oDB->connection));
496                         $iLines++;
497                         if ($iLines == 1000) {
498                             echo '.';
499                             $iLines = 0;
500                         }
501                     }
502                 }
503                 usleep(10);
504             }
505             fclose($hFile);
506
507             $bAnyBusy = true;
508             while ($bAnyBusy) {
509                 $bAnyBusy = false;
510                 for ($i = 0; $i < $this->iInstances; $i++) {
511                     if (pg_connection_busy($aDBInstances[$i]->connection)) $bAnyBusy = true;
512                 }
513                 usleep(10);
514             }
515             echo "\n";
516         }
517
518         info('Creating indexes on Tiger data');
519         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tiger_import_finish.sql');
520         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
521         $sTemplate = $this->replaceTablespace(
522             '{ts:aux-data}',
523             CONST_Tablespace_Aux_Data,
524             $sTemplate
525         );
526         $sTemplate = $this->replaceTablespace(
527             '{ts:aux-index}',
528             CONST_Tablespace_Aux_Index,
529             $sTemplate
530         );
531         $this->pgsqlRunScript($sTemplate, false);
532     }
533
534     public function calculatePostcodes($bCMDResultAll)
535     {
536         info('Calculate Postcodes');
537         if ($this->oDB == null) $this->oDB =& getDB();
538         if (!pg_query($this->oDB->connection, 'TRUNCATE location_postcode')) {
539             fail(pg_last_error($this->oDB->connection));
540         }
541
542
543         $sSQL  = 'INSERT INTO location_postcode';
544         $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
545         $sSQL .= "SELECT nextval('seq_place'), 1, country_code,";
546         $sSQL .= "       upper(trim (both ' ' from address->'postcode')) as pc,";
547         $sSQL .= '       ST_Centroid(ST_Collect(ST_Centroid(geometry)))';
548         $sSQL .= '  FROM placex';
549         $sSQL .= " WHERE address ? 'postcode' AND address->'postcode' NOT SIMILAR TO '%(,|;)%'";
550         $sSQL .= '       AND geometry IS NOT null';
551         $sSQL .= ' GROUP BY country_code, pc';
552
553         if (!pg_query($this->oDB->connection, $sSQL)) {
554             fail(pg_last_error($this->oDB->connection));
555         }
556
557         if (CONST_Use_Extra_US_Postcodes) {
558             // only add postcodes that are not yet available in OSM
559             $sSQL  = 'INSERT INTO location_postcode';
560             $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
561             $sSQL .= "SELECT nextval('seq_place'), 1, 'us', postcode,";
562             $sSQL .= '       ST_SetSRID(ST_Point(x,y),4326)';
563             $sSQL .= '  FROM us_postcode WHERE postcode NOT IN';
564             $sSQL .= '        (SELECT postcode FROM location_postcode';
565             $sSQL .= "          WHERE country_code = 'us')";
566             if (!pg_query($this->oDB->connection, $sSQL)) fail(pg_last_error($this->oDB->connection));
567         }
568
569         // add missing postcodes for GB (if available)
570         $sSQL  = 'INSERT INTO location_postcode';
571         $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
572         $sSQL .= "SELECT nextval('seq_place'), 1, 'gb', postcode, geometry";
573         $sSQL .= '  FROM gb_postcode WHERE postcode NOT IN';
574         $sSQL .= '           (SELECT postcode FROM location_postcode';
575         $sSQL .= "             WHERE country_code = 'gb')";
576         if (!pg_query($this->oDB->connection, $sSQL)) fail(pg_last_error($this->oDB->connection));
577
578         if (!$bCMDResultAll) {
579             $sSQL = "DELETE FROM word WHERE class='place' and type='postcode'";
580             $sSQL .= 'and word NOT IN (SELECT postcode FROM location_postcode)';
581             if (!pg_query($this->oDB->connection, $sSQL)) {
582                 fail(pg_last_error($this->oDB->connection));
583             }
584         }
585         $sSQL = 'SELECT count(getorcreate_postcode_id(v)) FROM ';
586         $sSQL .= '(SELECT distinct(postcode) as v FROM location_postcode) p';
587
588         if (!pg_query($this->oDB->connection, $sSQL)) {
589             fail(pg_last_error($this->oDB->connection));
590         }
591     }
592
593     public function index($bIndexNoanalyse)
594     {
595         $sOutputFile = '';
596         $sBaseCmd = CONST_InstallPath.'/nominatim/nominatim -i -d '.$this->aDSNInfo['database'].' -P '
597             .$this->aDSNInfo['port'].' -t '.$this->iInstances.$sOutputFile;
598         if (isset($this->aDSNInfo['hostspec']) && $this->aDSNInfo['hostspec']) {
599             $sBaseCmd .= ' -H '.$this->aDSNInfo['hostspec'];
600         }
601         if (isset($this->aDSNInfo['username']) && $this->aDSNInfo['username']) {
602             $sBaseCmd .= ' -U '.$this->aDSNInfo['username'];
603         }
604         $aProcEnv = null;
605         if (isset($this->aDSNInfo['password']) && $this->aDSNInfo['password']) {
606             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
607         }
608
609         info('Index ranks 0 - 4');
610         $iStatus = runWithEnv($sBaseCmd.' -R 4', $aProcEnv);
611         if ($iStatus != 0) {
612             fail('error status ' . $iStatus . ' running nominatim!');
613         }
614         if (!$bIndexNoanalyse) $this->pgsqlRunScript('ANALYSE');
615         info('Index ranks 5 - 25');
616         $iStatus = runWithEnv($sBaseCmd.' -r 5 -R 25', $aProcEnv);
617         if ($iStatus != 0) {
618             fail('error status ' . $iStatus . ' running nominatim!');
619         }
620         if (!$bIndexNoanalyse) $this->pgsqlRunScript('ANALYSE');
621         info('Index ranks 26 - 30');
622         $iStatus = runWithEnv($sBaseCmd.' -r 26', $aProcEnv);
623         if ($iStatus != 0) {
624             fail('error status ' . $iStatus . ' running nominatim!');
625         }
626         info('Index postcodes');
627         if ($this->oDB == null) $this->oDB =& getDB();
628         $sSQL = 'UPDATE location_postcode SET indexed_status = 0';
629         if (!pg_query($this->oDB->connection, $sSQL)) fail(pg_last_error($this->oDB->connection));
630     }
631
632     public function createSearchIndices()
633     {
634         info('Create Search indices');
635
636         $sTemplate = file_get_contents(CONST_BasePath.'/sql/indices.src.sql');
637         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
638         $sTemplate = $this->replaceTablespace(
639             '{ts:address-index}',
640             CONST_Tablespace_Address_Index,
641             $sTemplate
642         );
643         $sTemplate = $this->replaceTablespace(
644             '{ts:search-index}',
645             CONST_Tablespace_Search_Index,
646             $sTemplate
647         );
648         $sTemplate = $this->replaceTablespace(
649             '{ts:aux-index}',
650             CONST_Tablespace_Aux_Index,
651             $sTemplate
652         );
653         $this->pgsqlRunScript($sTemplate);
654     }
655
656     public function createCountryNames()
657     {
658         info('Create search index for default country names');
659
660         $this->pgsqlRunScript("select getorcreate_country(make_standard_name('uk'), 'gb')");
661         $this->pgsqlRunScript("select getorcreate_country(make_standard_name('united states'), 'us')");
662         $this->pgsqlRunScript('select count(*) from (select getorcreate_country(make_standard_name(country_code), country_code) from country_name where country_code is not null) as x');
663         $this->pgsqlRunScript("select count(*) from (select getorcreate_country(make_standard_name(name->'name'), country_code) from country_name where name ? 'name') as x");
664         $sSQL = 'select count(*) from (select getorcreate_country(make_standard_name(v),'
665             .'country_code) from (select country_code, skeys(name) as k, svals(name) as v from country_name) x where k ';
666         if (CONST_Languages) {
667             $sSQL .= 'in ';
668             $sDelim = '(';
669             foreach (explode(',', CONST_Languages) as $sLang) {
670                 $sSQL .= $sDelim."'name:$sLang'";
671                 $sDelim = ',';
672             }
673             $sSQL .= ')';
674         } else {
675             // all include all simple name tags
676             $sSQL .= "like 'name:%'";
677         }
678         $sSQL .= ') v';
679         $this->pgsqlRunScript($sSQL);
680     }
681
682     public function drop()
683     {
684         info('Drop tables only required for updates');
685
686         // The implementation is potentially a bit dangerous because it uses
687         // a positive selection of tables to keep, and deletes everything else.
688         // Including any tables that the unsuspecting user might have manually
689         // created. USE AT YOUR OWN PERIL.
690         // tables we want to keep. everything else goes.
691         $aKeepTables = array(
692                         '*columns',
693                         'import_polygon_*',
694                         'import_status',
695                         'place_addressline',
696                         'location_postcode',
697                         'location_property*',
698                         'placex',
699                         'search_name',
700                         'seq_*',
701                         'word',
702                         'query_log',
703                         'new_query_log',
704                         'spatial_ref_sys',
705                         'country_name',
706                         'place_classtype_*'
707                        );
708
709         if ($this->oDB = null) $this->oDB =& getDB();
710         $aDropTables = array();
711         $aHaveTables = chksql($this->oDB->getCol("SELECT tablename FROM pg_tables WHERE schemaname='public'"));
712
713         foreach ($aHaveTables as $sTable) {
714             $bFound = false;
715             foreach ($aKeepTables as $sKeep) {
716                 if (fnmatch($sKeep, $sTable)) {
717                     $bFound = true;
718                     break;
719                 }
720             }
721             if (!$bFound) array_push($aDropTables, $sTable);
722         }
723         foreach ($aDropTables as $sDrop) {
724             if ($this->sVerbose) echo "dropping table $sDrop\n";
725             @pg_query($this->oDB->connection, "DROP TABLE $sDrop CASCADE");
726             // ignore warnings/errors as they might be caused by a table having
727             // been deleted already by CASCADE
728         }
729
730         if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
731             if ($sVerbose) echo 'deleting '.CONST_Osm2pgsql_Flatnode_File."\n";
732             unlink(CONST_Osm2pgsql_Flatnode_File);
733         }
734     }
735
736     private function pgsqlRunDropAndRestore($sDumpFile)
737     {
738         if (!isset($this->aDSNInfo['port']) || !$this->aDSNInfo['port']) $this->aDSNInfo['port'] = 5432;
739         $sCMD = 'pg_restore -p '.$this->aDSNInfo['port'].' -d '.$this->aDSNInfo['database'].' -Fc --clean '.$sDumpFile;
740         if (isset($this->aDSNInfo['hostspec']) && $this->aDSNInfo['hostspec']) {
741             $sCMD .= ' -h '.$this->aDSNInfo['hostspec'];
742         }
743         if (isset($this->aDSNInfo['username']) && $this->aDSNInfo['username']) {
744             $sCMD .= ' -U '.$this->aDSNInfo['username'];
745         }
746         $aProcEnv = null;
747         if (isset($this->aDSNInfo['password']) && $this->aDSNInfo['password']) {
748             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
749         }
750         $iReturn = runWithEnv($sCMD, $aProcEnv);    // /lib/cmd.php "function runWithEnv($sCmd, $aEnv)"
751     }
752                      
753     private function pgsqlRunScript($sScript, $bfatal = true)
754     {
755         runSQLScript(
756             $sScript,
757             $bfatal,
758             $this->sVerbose,
759             $this->sIgnoreErrors
760         );
761     }
762
763     private function createSqlFunctions()
764     {
765         $sTemplate = file_get_contents(CONST_BasePath.'/sql/functions.sql');
766         $sTemplate = str_replace('{modulepath}', $this->sModulePath, $sTemplate);
767         if ($this->bEnableDiffUpdates) {
768             $sTemplate = str_replace('RETURN NEW; -- %DIFFUPDATES%', '--', $sTemplate);
769         }
770         if ($this->bEnableDebugStatements) {
771             $sTemplate = str_replace('--DEBUG:', '', $sTemplate);
772         }
773         if (CONST_Limit_Reindexing) {
774             $sTemplate = str_replace('--LIMIT INDEXING:', '', $sTemplate);
775         }
776         if (!CONST_Use_US_Tiger_Data) {
777             $sTemplate = str_replace('-- %NOTIGERDATA% ', '', $sTemplate);
778         }
779         if (!CONST_Use_Aux_Location_data) {
780             $sTemplate = str_replace('-- %NOAUXDATA% ', '', $sTemplate);
781         }
782         $this->pgsqlRunScript($sTemplate);
783     }
784
785     private function pgsqlRunPartitionScript($sTemplate)
786     {
787         if ($this->oDB == null) $this->oDB =& getDB();
788
789         $sSQL = 'select distinct partition from country_name';
790         $aPartitions = chksql($this->oDB->getCol($sSQL));
791         if (!$this->bNoPartitions) $aPartitions[] = 0;
792
793         preg_match_all('#^-- start(.*?)^-- end#ms', $sTemplate, $aMatches, PREG_SET_ORDER);
794         foreach ($aMatches as $aMatch) {
795             $sResult = '';
796             foreach ($aPartitions as $sPartitionName) {
797                 $sResult .= str_replace('-partition-', $sPartitionName, $aMatch[1]);
798             }
799             $sTemplate = str_replace($aMatch[0], $sResult, $sTemplate);
800         }
801
802         $this->pgsqlRunScript($sTemplate);
803     }
804
805     private function pgsqlRunScriptFile($sFilename)
806     {
807         if (!file_exists($sFilename)) fail('unable to find '.$sFilename);
808
809         $sCMD = 'psql -p '.$this->aDSNInfo['port'].' -d '.$this->aDSNInfo['database'];
810         if (!$this->sVerbose) {
811             $sCMD .= ' -q';
812         }
813         if (isset($this->aDSNInfo['hostspec']) && $this->aDSNInfo['hostspec']) {
814             $sCMD .= ' -h '.$this->aDSNInfo['hostspec'];
815         }
816         if (isset($this->aDSNInfo['username']) && $this->aDSNInfo['username']) {
817             $sCMD .= ' -U '.$this->aDSNInfo['username'];
818         }
819         $aProcEnv = null;
820         if (isset($this->aDSNInfo['password']) && $this->aDSNInfo['password']) {
821             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
822         }
823         $ahGzipPipes = null;
824         if (preg_match('/\\.gz$/', $sFilename)) {
825             $aDescriptors = array(
826                              0 => array('pipe', 'r'),
827                              1 => array('pipe', 'w'),
828                              2 => array('file', '/dev/null', 'a')
829                             );
830             $hGzipProcess = proc_open('zcat '.$sFilename, $aDescriptors, $ahGzipPipes);
831             if (!is_resource($hGzipProcess)) fail('unable to start zcat');
832             $aReadPipe = $ahGzipPipes[1];
833             fclose($ahGzipPipes[0]);
834         } else {
835             $sCMD .= ' -f '.$sFilename;
836             $aReadPipe = array('pipe', 'r');
837         }
838         $aDescriptors = array(
839                          0 => $aReadPipe,
840                          1 => array('pipe', 'w'),
841                          2 => array('file', '/dev/null', 'a')
842                         );
843         $ahPipes = null;
844         $hProcess = proc_open($sCMD, $aDescriptors, $ahPipes, null, $aProcEnv);
845         if (!is_resource($hProcess)) fail('unable to start pgsql');
846         // TODO: error checking
847         while (!feof($ahPipes[1])) {
848             echo fread($ahPipes[1], 4096);
849         }
850         fclose($ahPipes[1]);
851         $iReturn = proc_close($hProcess);
852         if ($iReturn > 0) {
853             fail("pgsql returned with error code ($iReturn)");
854         }
855         if ($ahGzipPipes) {
856             fclose($ahGzipPipes[1]);
857             proc_close($hGzipProcess);
858         }
859     }
860
861     private function replaceTablespace($sTemplate, $sTablespace, $sSql)
862     {
863         if ($sTablespace) {
864             $sSql = str_replace($sTemplate, 'TABLESPACE "'.$sTablespace.'"', $sSql);
865         } else {
866             $sSql = str_replace($sTemplate, '', $sSql);
867         }
868         return $sSql;
869     }
870 }