@@ -10,7 +10,8 @@ import { Logger } from "./index.js";
1010import { PlacesSearcher } from "./services/PlacesSearcher.js" ;
1111import { fileURLToPath } from "url" ;
1212import { dirname } from "path" ;
13- import { readFileSync } from "fs" ;
13+ import { readFileSync , writeFileSync , existsSync } from "fs" ;
14+ import { createInterface } from "readline" ;
1415
1516// Get the directory of the current module
1617const __filename = fileURLToPath ( import . meta. url ) ;
@@ -90,6 +91,8 @@ const EXEC_TOOLS = [
9091 "plan-route" ,
9192 "compare-places" ,
9293 "air-quality" ,
94+ "static-map" ,
95+ "batch-geocode-tool" ,
9396] as const ;
9497
9598async function execTool ( toolName : string , params : any , apiKey : string ) : Promise < any > {
@@ -177,6 +180,26 @@ async function execTool(toolName: string, params: any, apiKey: string): Promise<
177180 params . includePollutants
178181 ) ;
179182
183+ case "static-map" :
184+ case "maps_static_map" :
185+ return searcher . getStaticMap ( params ) ;
186+
187+ case "batch-geocode-tool" :
188+ case "maps_batch_geocode" : {
189+ const results = await Promise . all (
190+ ( params . addresses as string [ ] ) . map ( async ( address : string ) => {
191+ try {
192+ const result = await searcher . geocode ( address ) ;
193+ return { address, ...result } ;
194+ } catch ( error : any ) {
195+ return { address, success : false , error : error . message } ;
196+ }
197+ } )
198+ ) ;
199+ const succeeded = results . filter ( ( r ) => r . success ) . length ;
200+ return { success : true , data : { total : params . addresses . length , succeeded, failed : params . addresses . length - succeeded , results } } ;
201+ }
202+
180203 default :
181204 throw new Error ( `Unknown tool: ${ toolName } . Available: ${ EXEC_TOOLS . join ( ", " ) } ` ) ;
182205 }
@@ -257,6 +280,127 @@ if (isRunDirectly || isMainModule) {
257280 }
258281 }
259282 )
283+ . command (
284+ "batch-geocode" ,
285+ "Geocode multiple addresses from a file (one address per line)" ,
286+ ( yargs ) => {
287+ return yargs
288+ . option ( "input" , {
289+ alias : "i" ,
290+ type : "string" ,
291+ describe : "Input file path (one address per line). Use - for stdin." ,
292+ demandOption : true ,
293+ } )
294+ . option ( "output" , {
295+ alias : "o" ,
296+ type : "string" ,
297+ describe : "Output file path (JSON). Defaults to stdout." ,
298+ } )
299+ . option ( "concurrency" , {
300+ alias : "c" ,
301+ type : "number" ,
302+ describe : "Max parallel requests" ,
303+ default : 20 ,
304+ } )
305+ . option ( "apikey" , {
306+ alias : "k" ,
307+ type : "string" ,
308+ description : "Google Maps API key" ,
309+ default : process . env . GOOGLE_MAPS_API_KEY ,
310+ } )
311+ . example ( [
312+ [ "$0 batch-geocode -i addresses.txt" , "Geocode to stdout" ] ,
313+ [ "$0 batch-geocode -i addresses.txt -o results.json" , "Geocode to file" ] ,
314+ [ "cat addresses.txt | $0 batch-geocode -i -" , "Geocode from stdin" ] ,
315+ ] ) ;
316+ } ,
317+ async ( argv ) => {
318+ if ( ! argv . apikey ) {
319+ console . error ( "Error: GOOGLE_MAPS_API_KEY not set. Use --apikey or set env var." ) ;
320+ process . exit ( 1 ) ;
321+ }
322+
323+ // Read addresses
324+ let lines : string [ ] ;
325+ if ( argv . input === "-" ) {
326+ // Read from stdin
327+ const rl = createInterface ( { input : process . stdin } ) ;
328+ lines = [ ] ;
329+ for await ( const line of rl ) {
330+ const trimmed = line . trim ( ) ;
331+ if ( trimmed ) lines . push ( trimmed ) ;
332+ }
333+ } else {
334+ if ( ! existsSync ( argv . input as string ) ) {
335+ console . error ( `Error: File not found: ${ argv . input } ` ) ;
336+ process . exit ( 1 ) ;
337+ }
338+ lines = readFileSync ( argv . input as string , "utf-8" )
339+ . split ( "\n" )
340+ . map ( ( l ) => l . trim ( ) )
341+ . filter ( ( l ) => l . length > 0 ) ;
342+ }
343+
344+ if ( lines . length === 0 ) {
345+ console . error ( "Error: No addresses found in input." ) ;
346+ process . exit ( 1 ) ;
347+ }
348+
349+ const searcher = new PlacesSearcher ( argv . apikey as string ) ;
350+ const concurrency = Math . min ( Math . max ( argv . concurrency as number , 1 ) , 50 ) ;
351+ const results : any [ ] = [ ] ;
352+ let completed = 0 ;
353+
354+ // Process with concurrency limit
355+ const semaphore = async ( tasks : ( ( ) => Promise < void > ) [ ] , limit : number ) => {
356+ const executing : Promise < void > [] = [];
357+ for (const task of tasks) {
358+ const p = task ( ) . then ( ( ) => {
359+ executing . splice ( executing . indexOf ( p ) , 1 ) ;
360+ } ) ;
361+ executing . push ( p ) ;
362+ if ( executing . length >= limit ) {
363+ await Promise . race ( executing ) ;
364+ }
365+ }
366+ await Promise . all ( executing ) ;
367+ } ;
368+
369+ const tasks = lines . map ( ( address , index ) => async ( ) => {
370+ try {
371+ const result = await searcher . geocode ( address ) ;
372+ results [ index ] = { address, ...result } ;
373+ } catch ( error : any ) {
374+ results [ index ] = { address, success : false , error : error . message } ;
375+ }
376+ completed ++ ;
377+ if ( ! argv . output ) return ; // Don't log progress when outputting to stdout
378+ process . stderr . write ( `\r ${ completed } /${ lines . length } geocoded` ) ;
379+ } ) ;
380+
381+ await semaphore ( tasks , concurrency ) ;
382+
383+ if ( argv . output ) {
384+ process . stderr . write ( "\n" ) ;
385+ }
386+
387+ // Summary
388+ const succeeded = results . filter ( ( r ) => r . success ) . length ;
389+ const failed = results . filter ( ( r ) => ! r . success ) . length ;
390+ const summary = { total : lines . length , succeeded, failed, results } ;
391+
392+ const json = JSON . stringify ( summary , null , 2 ) ;
393+
394+ if ( argv . output ) {
395+ writeFileSync ( argv . output as string , json , "utf-8" ) ;
396+ console . error ( `Done: ${ succeeded } /${ lines . length } succeeded. Output: ${ argv . output } ` ) ;
397+ } else {
398+ console. log ( json ) ;
399+ }
400+
401+ process . exit ( failed > 0 ? 1 : 0 ) ;
402+ }
403+ )
260404 . command (
261405 "$0" ,
262406 "Start the MCP server (HTTP by default, --stdio for stdio mode)" ,
0 commit comments