*! v1.1.4 SConcas June242026 Sisinnio Concas concas@usf.edu
capture program drop lodes
program lodes
    version 16.1
    gettoken subcmd 0 : 0, parse(" ,")
    _lodes_parse_subcmd `subcmd'
    lodes_`subcmd' `macval(0)'
    if `"`local'"'!="" { // pass through returns
        c_local `local' `"`value'"'
    }
end

capture program drop _lodes_parse_subcmd
program _lodes_parse_subcmd
    version 16.1
    local l = strlen(`"`0'"')
	if `"`0'"'=="get"											local 0 get
    else if `"`0'"'== substr("area_profile", 1, max(4,`l')) 	local 0 area_profile
	else if `"`0'"'== substr("destination", 1, max(4,`l')) 		local 0 destination
    else if `"`0'"'== substr("inflow_outflow", 1, max(4,`l')) 	local 0 inflow_outflow
	capt mata: assert(st_islmname(st_local("0")))
    if _rc==1 exit _rc
    if _rc {
        di as err `"`0' invalid subcommand"'
        exit 198
    }
    c_local subcmd `"`0'"'
end

********************************************************************************
********************************Get*********************************************
********************************************************************************
capture program drop lodes_get
program lodes_get
		version 16.1
		syntax	[anything(name=state)], ///
				[DATA_type(string)][JOB_type(string) SEGment(string)] ///
				[YEARs(string)] [od_part(string) LODEs_version(string)]	///
				[saveas(string) EXportexcel saveframes]				///
				[replace BRowse CLear]

		// NOTE: we do NOT call `frames reset` here. Doing so would silently
		// destroy any frames the user has open. Instead, downloads run in a
		// dedicated scratch frame (created below) and only frames lodes
		// creates may be overwritten -- and only if the user passes clear.

		// check arguments have been provided
		if "`state'" == "" {
			display as error "{p} You must specify a state abbreviation (e.g., fl). {p_end}"
			exit
		}
		// Normalize state to lower case (LODES URLs and frame names use lower).
		// Done early so downstream validation/frame-collision checks see the
		// canonical form.
		local state = lower("`state'")

		**data type
		local data_type: di lower("`data_type'")
		if "`data_type'" == "" {
			local data_type od
			display as text "{p} Default data type is {result:`data_type'}. To change, setup {input:{it: data_type}}{p_end}"
			*exit
		}
		else if !inlist("`data_type'", "od", "wac", "rac") {
			display as error "{p} You must specify which data type: 'rac', 'wac', or 'od{p_end}"
			exit
		}

		** years
		if "`years'" == "" {
			local years = 2023
			display as text "{p} Using latest year ({result:`years'}) as default. To change, setup {input:{it:years}}{p_end}"
		}


		**job type
		local job_type: di upper("`job_type'")
		if "`job_type'" == "" {
			local job_type = "JT00"
			display as text "{p} Using value of {result:`job_type'} (All Jobs) as default for job type.To change, setup {input:{it:job_type}}{p_end}"
		}

		**segment (only meaningful for WAC and RAC; OD CSVs aren't
		**segmented, so the option is ignored for OD with a warning).
		local segment : di upper("`segment'")
		if "`data_type'" == "od" {
			if "`segment'" != "" {
				display as text "{p} The {input:{it:segment}} option (here: {result:`segment'}) does not apply to {result:OD} data and is being ignored.{p_end}"
				local segment ""
			}
		}
		else {
			if "`segment'" == "" {
				local segment = "S000"
				display as text "{p} Using value of {result:`segment'} (Total number of jobs) as default for segment. To change, setup {input:{it:segment}}{p_end}"
			}
		}

		**O-D file state part (main or aux)
		if "`od_part'" == "" {
			local od_part = "main aux"
			display as text "{p} Using {result:`od_part'} as default for state file in O-D data.To change, setup {input:{it:od_part}}{p_end}"

		}

		**LODES version
		if "`lodes_version'" == "" {
			local lodes_version = "LODES8"
			display as text "{p} Using {result:`lodes_version'} as default. To change, setup {input:{it:lodes_version}}{p_end}"
		}

		// prelim checks and parsing  ------------------------------------------

		// Refuse to clobber pre-existing output frames unless user passes clear.
		// The frames lodes_get creates and leaves behind are:
		//   <state>_xwalk
		//   <state>_<data_type>_<job_type>
		local _lodes_outframes `state'_xwalk `state'_`data_type'_`job_type'
		local _lodes_conflicts
		foreach fr of local _lodes_outframes {
			capture quietly frame `fr': describe
			if _rc == 0 local _lodes_conflicts `_lodes_conflicts' `fr'
		}
		if "`_lodes_conflicts'" != "" {
			if "`clear'" == "" {
				di as error "{p}The following frame(s) already exist: " ///
					"{result:`_lodes_conflicts'}. Pass option {bf:clear} " ///
					"to overwrite, or drop/rename them first.{p_end}"
				exit 110
			}
			foreach fr of local _lodes_conflicts {
				frame drop `fr'
			}
		}

		// Remember the user's entry frame so error paths can restore it.
		local _entry_frame = c(frame)

		// Create a scratch frame for downloads. gzimport's `clear` option wipes
		// the *current* frame's data, so doing this in default would destroy
		// the user's working dataset. We work in the scratch frame instead
		// and only frame-put results into the user-visible output frames.
		local _lodes_scratch __lodes_get_scratch__
		capture frame drop `_lodes_scratch'
		frame create `_lodes_scratch'
		frame change `_lodes_scratch'

		// Detect required user-written packages and, if any are missing,
		// instruct the user to install them and exit. We intentionally do
		// NOT silently install on the user's behalf: the user should know
		// what is being added to their ado-path.
		local _missing_ssc
		foreach d in fframeappend geoinpoly {
			capture which "`d'"
			if _rc local _missing_ssc `_missing_ssc' `d'
		}
		capture which gzimport
		local _missing_gzimport = cond(_rc, 1, 0)

		if "`_missing_ssc'" != "" | `_missing_gzimport' {
			di as error "{p}{bf:lodes} requires the following user-written package(s), which are not currently installed:{p_end}"
			foreach d of local _missing_ssc {
				di as error `"    {bf:`d'} (from SSC) -- install with: {cmd:ssc install `d'}"'
			}
			if `_missing_gzimport' {
				di as error `"    {bf:gzimport} (from GitHub) -- install with: {cmd:net install gzimport, from(https://raw.githubusercontent.com/mdroste/stata-gzimport/master/)}"'
			}
			di as error "{p}Install the missing package(s) and re-run {cmd:lodes get}. (See {bf:help lodes##requirements} for details.){p_end}"

			// Clean up the scratch frame we created above so we don't leave
			// debris behind when we abort.
			capture frame change `_entry_frame'
			capture frame drop `_lodes_scratch'
			exit 199
		}


		// (state already lower-cased earlier; see normalization above)

		// parse years
		local years = ustrregexra("`years'", "-", "/")
		qui numlist "`years'", sort
		local years = "`r(numlist)'"

		//make call to download data
		local url_base "https://lehd.ces.census.gov/data/lodes/`lodes_version'/`state'"

		**check for mispelled state
		lodes_state_check "`url_base'" "`state'"

		**check for data availability
		lodes_data_check "`state'" "`data_type'" "`years'"


		*state xwalk reference call (runs in the scratch frame)
		capture qui gzimport delimited using "`url_base'/`state'_xwalk.csv.gz", stringcols(_all) clear
		if _rc {
			local _save_rc = _rc
			di as error `"{p}Failed to download crosswalk for state {result:`state'} from {result:`url_base'/`state'_xwalk.csv.gz} (rc=`_save_rc'). Check network connectivity and that the state has LODES data available.{p_end}"'
			capture frame change `_entry_frame'
			capture frame drop `_lodes_scratch'
			exit `_save_rc'
		}
		capture frame drop `state'_xwalk
		frame put *, into(`state'_xwalk)


		*dataset call
		*"https://lehd.ces.census.gov/data/lodes/LODES8/fl/od/fl_od_aux_JT00_2010.csv.gz"
		if "`data_type'" == "od" {
			local files "`od_part'"
			foreach y of local years {
				foreach f of local files {
					capture qui gzimport delimited using "`url_base'/`data_type'/`state'_`data_type'_`f'_`job_type'_`y'.csv.gz" ,stringcols(1 2) clear
					if _rc {
						local _save_rc = _rc
						di as error `"{p}Failed to download {result:`data_type'} data for state {result:`state'}, year {result:`y'}, part {result:`f'}, job type {result:`job_type'} (rc=`_save_rc'). Check network connectivity and per-state year availability in the LODES tech doc.{p_end}"'
						capture frame change `_entry_frame'
						capture frame drop `_lodes_scratch'
						capture frame drop `state'_xwalk
						exit `_save_rc'
					}
					gen year = `y'
					gen od_type = "`f'"
					capture frame drop `state'_`data_type'_`job_type'_`y'_`f'
					frame put *, into(`state'_`data_type'_`job_type'_`y'_`f')
				}
			}
		}

		else {
			foreach y of local years {
				capture qui gzimport delimited using "`url_base'/`data_type'/`state'_`data_type'_`segment'_`job_type'_`y'.csv.gz", stringcols(1) clear
				if _rc {
					local _save_rc = _rc
					di as error `"{p}Failed to download {result:`data_type'} data for state {result:`state'}, year {result:`y'}, segment {result:`segment'}, job type {result:`job_type'} (rc=`_save_rc'). Check network connectivity and per-state year availability in the LODES tech doc.{p_end}"'
					capture frame change `_entry_frame'
					capture frame drop `_lodes_scratch'
					capture frame drop `state'_xwalk
					exit `_save_rc'
				}
				gen year = `y'
				capture frame drop `state'_`data_type'_`job_type'_`y'
				frame put *, into(`state'_`data_type'_`job_type'_`y')
			}
		}


		// Append frames
		frame create `state'_`data_type'_`job_type'
		frame change `state'_`data_type'_`job_type'
		*frame list
		if "`data_type'" == "od" {
			local files "`od_part'"
				foreach y of local years {
					foreach f of local files {
						fframeappend, using(`state'_`data_type'_`job_type'_`y'_`f')
						frame drop `state'_`data_type'_`job_type'_`y'_`f'
				}

			}
		}

		else {
			foreach y of local years {
				fframeappend, using(`state'_`data_type'_`job_type'_`y')
				frame drop `state'_`data_type'_`job_type'_`y'
				}
		}



		// Clean up our scratch frame; leave the user's default frame alone.
		// (Previously this dropped the default frame, which destroyed any
		// data the user had loaded before calling lodes.)
		capture frame drop `_lodes_scratch'
		qui frame dir

		//Add links from xwalk file
		if "`data_type'" == "od" {
			*link frame to identify blocks where workers live (residence block code)
			qui frlink m:1 h_geocode, frame(`state'_xwalk tabblk2020) generate(xwalk_h)
			*link frame to identify blocks where workers work (workplace block code)
			qui frlink m:1 w_geocode, frame(`state'_xwalk tabblk2020) generate(xwalk_w)
		}

		if "`data_type'" == "rac" {
			*link frame to identify blocks where workers live (residence block code)
			qui frlink m:1 h_geocode, frame(`state'_xwalk tabblk2020) generate(xwalk_h)
		}

		if "`data_type'" == "wac" {
			*link frame to identify blocks where workers work (workplace block code)
			qui frlink m:1 w_geocode, frame(`state'_xwalk tabblk2020) generate(xwalk_w)
		}


		// variable labeling
		la var createdate "Year data created"
		la var year "Database year"

		if "`data_type'" == "od" {
			la var xwalk_h "Frame link residence block code"
			la var xwalk_w "Frame link workplace block code"
		}
		if "`data_type'" == "rac" {
			la var xwalk_h "Frame link residence block code"
		}
		if "`data_type'" == "wac" {
			la var xwalk_w "Frame link workplace block code"
		}

		if "`data_type'" == "od" {
			la var od_type "State part file"
		}


		//Export Data
		if "`saveas'" != "" {
			local fr_opt
			if "`saveframes'" != "" {
				local fr_opt "saveframes(`state'_xwalk `state'_`data_type'_`job_type')"
			}
			lodes_save_output, saveas(`"`saveas'"') `replace' `fr_opt'
		}

		`browse'

end

********************************************************************************
******************************Destination***************************************
********************************************************************************
capture program drop lodes_destination
program lodes_destination
		version 16.1
		syntax	[anything(name=oname)], 									///
				[origin(string) dname(string) ] 	///
				[oname_id(string) top_loc(string) shapedir(string)]							///
				[saveas(string) EXportexcel saveframes]					///
				[replace deleteframe BRowse]
				*clear SAVEas(string)

		// Validate that the current frame was created by `lodes get' with
		// OD data and set up in_frame, data_type, state, xwalk locals.
		lodes_check_input_frame od


		// entries checks
		**oname list
		if "`oname'" == "" {
			di as error `"{p} You must specify a geography of origin (bgrp, trct, zcta, cty, stplc, cbsa, st){p_end}"'
			exit 198
		}

		// Determine whether oname is a standard geography or a custom polygon.
		// `oname_poly' is reused later (top_loc keep, xwalk cleanup).
		lodes_is_polygon `oname'
		local oname_poly = `is_polygon'

		// Custom polygon: hand off to the shapefile-processing helper
		if `oname_poly' {
			local name "`oname'"
			lodes_polygon_process "`name'" "`shapedir'"
		}

		**origin type check
		if "`origin'" == "" {
			local origin home
			di as text `"{p} Default origin is: {result:home}. To change, specify {input:{it: origin}} value {p_end}"'

		}
		else {
			if !inlist("`origin'", "home", "work") {
				di as error `"{p} You must specify either "home" or "work" as origin{p_end}"'
				exit 198
			}
		}


		**otype assign
		local otype =substr("`origin'",1,1)

		** dname list
		if "`dname'" == "" {
			di as error `"{p} You must specify a geography of destination (bgrp, trct, zcta, cty, stplc, cbsa, st).{p_end}"'
			exit 198
		}
		// dname must be a standard geography; custom polygons are not allowed as a destination
		lodes_is_polygon `dname'
		if `is_polygon' {
			di as error `"{p} Destination geography ({result:`dname'}) is not a recognised standard geography (bgrp, trct, zcta, cty, stplc, cbsa, st). Custom polygons are only supported as origins.{p_end}"'
			exit 198
		}

		**dtype assign
		if "`otype'" == "w" {
			local dtype h
		}
		if "`otype'" == "h" {
			local dtype w
		}


		//process data

		*define frame output name (max 32 characters)
		local out_frame `in_frame'_`oname'_`otype'
		lodes_check_frame_name `out_frame'

		qui frame copy 		`in_frame'								///
						`out_frame', replace

		frame change 	`out_frame'

		// add aliases based on selected geography
		qui fralias add `oname'_`otype' = `oname', from(xwalk_`otype') // geography ID of origin
		la var `oname'_`otype' "`oname'_`otype' code"

		qui fralias add `oname'_name_`otype' = `oname'name, from(xwalk_`otype') // geography name of origin
		la var `oname'_name_`otype' "`oname'_name_`otype' name"

		qui fralias add `dname'_`dtype' = `dname', from(xwalk_`dtype') // geography ID of destination
		la var `dname'_`dtype' "`dname'_`dtype' code"

		qui fralias add `dname'_name_`dtype' = `dname'name, from(xwalk_`dtype') // geography name  of destination
		la var `dname'_name_`dtype' "`dname'_name_`dtype' name"

		// aggregate by selected geography
		egen s000_`oname'_`otype'_`dname'_`dtype' = sum(s000), by(`oname'_`otype' `dname'_`dtype' year)
		la var s000_`oname'_`otype'_`dname'_`dtype'  "Job counts by `oname'_`otype'_`dname'_`dtype'"

		qui duplicates drop `oname'_`otype' `dname'_`dtype' s000_`oname'_`otype'_`dname'_`dtype' year, force

		gsort -s000_`oname'_`otype'_`dname'_`dtype' year


		// Options

		**generate thresholds by location

		egen workers_`oname'_`otype' = sum(s000_`oname'_`otype'_`dname'_`dtype'), by (`oname'_`otype' year)
		la var workers_`oname'_`otype' "Total workers in `oname'_`otype'"

		gen workers_`dname'_`dtype'_sh = 100*(s000_`oname'_`otype'_`dname'_`dtype'/workers_`oname'_`otype')
		la var workers_`dname'_`dtype'_sh "Share of `dname'_`dtype' workers by `oname'_`otype'"

		gsort  `oname'_`otype' year -workers_`dname'_`dtype'_sh

		by  `oname'_`otype' year: gen `dname'_`dtype'_cum_share = sum(workers_`dname'_`dtype'_sh)
		la var `dname'_`dtype'_cum_share "Cumulative share of `dname'_`dtype' workers by `oname'_`otype'"

		**remove locations as below before ranking
		qui drop if od_type == "aux"  //removing workers residing in other states
		qui drop if `dname'_`dtype' == "9999999" //undefined destinations because outside of origin

		egen `dname'_`dtype'_rank = rank(-workers_`dname'_`dtype'_sh), by (`oname'_`otype' year)
		la var `dname'_`dtype'_rank "Rank by jobs counts/share in `dname'_`dtype'"

		**recast linked variables
		qui frunalias 	`oname'_`otype'  `oname'_name_`otype' `dname'_`dtype' `dname'_name_`dtype'


		**retain only results by top x locations
		local top_loc = real("`top_loc'")
		if `top_loc' == . {
			di as text "{p}No specific threshold defined, entire output dataset retained. To change, specify {input:{it:top_loc}}{p_end}"
		}

		**polygon case
		if `top_loc'!= . & `oname_poly' {
			qui keep if `dname'_`dtype'_rank <= `top_loc' & `oname'_`otype' == 1
			di as result "{p}retaining only the top `top_loc' output observations{p_end}"
		}

		**all others
		else {
			if `top_loc'!= . {
				qui keep if `dname'_`dtype'_rank <= `top_loc'
			di as result "{p}retaining only the top `top_loc' output observations{p_end}"
			}
		}
		**process by selected geography of origin
		if "`oname_id'" == "" {
			di as text "{p} No specific origin geography ID defined, entire output dataset retained. To change, specify {input:{it:oname_id}}{p_end}"
		}

		else {
				qui keep if `oname'_`otype' == "`oname_id'"
				gsort  `oname'_`otype' year -s000_`oname'_`otype'_`dname'_`dtype'
				di as text "{p}Retaining only data on geography - {result:`oname' `oname_id'} - selected as origin type: {result:`origin'} {p_end}"
		}

		//retain only relevant variables
		keep 	year `oname'_`otype' `oname'_name_`otype' 	///
				`dname'_`dtype' `dname'_name_`dtype' 		///
				s000_`oname'_`otype'_`dname'_`dtype' 		///
				workers_`oname'_`otype' 					///
				workers_`dname'_`dtype'_sh 					///
				`dname'_`dtype'_cum_share 					///
				`dname'_`dtype'_rank

		//Export Data
		if "`saveas'" != "" {
			local fr_opt
			if "`saveframes'" != "" local fr_opt "saveframes(`out_frame')"
			local ex_opt
			if "`exportexcel'" != "" local ex_opt "exportexcel"
			lodes_save_output, saveas(`"`saveas'"') `replace' `ex_opt' `fr_opt'
		}

		//clean up data frames
		if "`deleteframe'" != "" {
			frame change `in_frame'
			frame drop `out_frame'
		}


		// clean xwalk file if polygon analysis was conducted
		if `oname_poly' {
			frame change `xwalk'
			drop `oname' `oname'name
		}

		//return to main frame
		frame change `in_frame'

		// browse if frame retained
		if "`deleteframe'" =="" {
			frame change `out_frame'
			`browse'
		}
end

********************************************************************************
**************************Inflow-Outflow****************************************
********************************************************************************
capture program drop lodes_inflow_outflow
program lodes_inflow_outflow
		version 16.1
		syntax	[anything(name=geography)], 							///
				[geo_id(string) work_seg(string) shapedir(string)] 		///
				[saveas(string) EXportexcel saveframes]				///
				[replace deleteframe BRowse]

		// Validate that the current frame was created by `lodes get' with
		// OD data and set up in_frame, data_type, state, xwalk locals.
		lodes_check_input_frame od

		// entries checks

		**geography list
		if "`geography'" == "" {
			di as error `"{p} You must specify a geography of origin (bgrp, trct, zcta, cty, stplc, cbsa, st).{p_end}"'
			exit 198
		}
		// Determine whether geography is a standard one or a custom polygon.
		// `geog_poly' is reused later in the dedup step.
		lodes_is_polygon `geography'
		local geog_poly = `is_polygon'

		if `geog_poly' {
			local name "`geography'"
			lodes_polygon_process "`name'" "`shapedir'"
		}


		// Copy data to new frame
		local out_frame `in_frame'_`geography'
		lodes_check_frame_name `out_frame'
		qui frame copy 	`in_frame'	///
						`out_frame', replace

		frame change `out_frame'

		// Add aliases based on selected geography

		qui fralias add `geography'_h = `geography', from(xwalk_h) // geography ID of origin
		la var `geography'_h "`geography'_h code"

		qui fralias add `geography'_name_h = `geography'name, from(xwalk_h) // geography name of origin
		la var `geography'_name_h "`geography'_name_h name"

		qui fralias add `geography'_w = `geography', from(xwalk_w) // geography ID of destination
		la var `geography'_w "`geography'_w code"

		qui fralias add `geography'_name_w = `geography'name, from(xwalk_w) // geography name  of destination
		la var `geography'_name_w "`geography'_name_w name"

		// Estimate Inflow/Outflow Job Counts
		// Note: previously this was two near-identical loops (with vs without
		// `qui`); they are now a single loop that always quiets egen output.
		local work_seg : di lower("`work_seg'")
		if "`work_seg'" == "" {
			local jobs s000
		}
		else {
			local jobs `work_seg'
		}
		foreach v of local jobs {
			qui egen live_area_`v' = sum(`v'), by(`geography'_h year)
			la var live_area_`v' "Living in the Selection Area job `v'"

			qui egen live_area_emp_out_`v'_ = sum(`v') if `geography'_h != `geography'_w, by(`geography'_h year)
			qui egen live_area_emp_out_`v' = max(live_area_emp_out_`v'_), by(`geography'_h year)
			drop live_area_emp_out_`v'_
			la var live_area_emp_out_`v' "Living in the Selection Area but Employed Outside job `v'"

			qui gen live_area_emp_in_`v'_ = live_area_`v' - live_area_emp_out_`v'
			qui egen live_area_emp_in_`v' = max(live_area_emp_in_`v'_), by(`geography'_h year)
			drop live_area_emp_in_`v'_
			la var live_area_emp_in_`v' "Living and Employed in the Selection Area job `v'"

			qui egen emp_area_`v' = sum(`v'), by(`geography'_w year)
			la var emp_area_`v' "Employed in the Selection Area job `v'"

			qui egen emp_area_lvng_out_`v'_ = sum(`v') if `geography'_h != `geography'_w, by(`geography'_w year)
			qui egen emp_area_lvng_out_`v' = max(emp_area_lvng_out_`v'_), by(`geography'_w year)
			drop emp_area_lvng_out_`v'_
			la var emp_area_lvng_out_`v' "Employed in the Selection Area but Living Outside job `v'"

			qui gen emp_area_lvng_in_`v'_ = emp_area_`v' - emp_area_lvng_out_`v'
			qui egen emp_area_lvng_in_`v' = max(emp_area_lvng_in_`v'_), by(`geography'_w year)
			drop emp_area_lvng_in_`v'_
			la var emp_area_lvng_in_`v' "Employed and Living in the Selection Area job `v'"
		}

		// Recast linked variables
		qui frunalias 	`geography'_h  `geography'_name_h 	///
					`geography'_w `geography'_name_w

		// Deduplicate to retain geography with Inflow/Outflow results
		if !`geog_poly' {
			**US Census geographies
			keep if `geography'_h == `geography'_w
			qui duplicates drop `geography'_h `geography'_w year, force
		}
		else {
			**polygon
			keep if `geography'_h == 1 & `geography'_h == `geography'_w
			qui duplicates drop emp_area_lvng_in_*, force
		}

		sort  `geography'_h year

		//clean up variables
		qui drop 	w_geocode h_geocode createdate od_type xwalk_h xwalk_w ///
					s000 sa01-sa03 se01-se03 si01-si03 ///
					`geography'_w `geography'_name_w
		rename (`geography'_h  `geography'_name_h) (`geography' `geography'_name )
		la var year "Year"

		//process by selected geography
		if "`geo_id'" == "" {
			di as text "{p}no specific geography defined, entire inflow-outflow dataset retained unless custom polygon being used. {p_end}"
		}

		else {
			if "`geo_id'" != "" {
				qui keep if `geography' == "`geo_id'"
				di as text "{p}Retaining ouput for geography:{result:`geography'} and geography id: {result:`geo_id'}. {p_end}"
				sort year
			}
		}

*******************************************************************************
		//Export Data
		if "`saveas'" != "" {
			local fr_opt
			if "`saveframes'" != "" local fr_opt "saveframes(`out_frame')"
			local ex_opt
			if "`exportexcel'" != "" local ex_opt "exportexcel"
			lodes_save_output, saveas(`"`saveas'"') `replace' `ex_opt' `fr_opt'
		}

		//clean up data frames
		if "`deleteframe'" != "" {
			frame change `in_frame'
			frame drop `out_frame'
		}

		//return to main frame
		frame change `in_frame'
		frame list

		// browse if frame retained
		if "`deleteframe'" =="" {
			frame change `out_frame'
			`browse'
		}
end

********************************************************************************
**************************Area_Profile******************************************
********************************************************************************
capture program drop lodes_area_profile
program lodes_area_profile
		version 16.1
		syntax	[anything(name=geography)], 							///
				[geo_id(string) work_seg(string) shapedir(string)] 		///
				[saveas(string) EXportexcel saveframes]				///
				[replace deleteframe BRowse]

		// Validate that the current frame was created by `lodes get' with
		// WAC or RAC data and set up in_frame, data_type, state, xwalk locals.
		lodes_check_input_frame wac rac

		// entries checks
		**geography list
		if "`geography'" == "" {
			di as error `"{p} You must specify a geography of origin (bgrp, trct, zcta, cty, stplc, cbsa, st).{p_end}"'
			exit 198
		}

		// Determine whether geography is a standard one or a custom polygon.
		// `geog_poly' is reused later in the dedup step.
		lodes_is_polygon `geography'
		local geog_poly = `is_polygon'

		if `geog_poly' {
			local name "`geography'"
			lodes_polygon_process "`name'" "`shapedir'"
		}


		// Copy data to new frame
		local out_frame `in_frame'_`geography'
		lodes_check_frame_name `out_frame'
		qui frame copy `in_frame' ///
		`out_frame', replace

		frame change `out_frame'
		drop createdate //not needed for analysis

		// Add aliases based on selected geography
		if "`data_type'" == "rac" {
			fralias add `geography', from(xwalk_h)
			la var `geography' "`geography' code"
			fralias add `geography'_name = `geography'name, from(xwalk_h)
			la var `geography'_name "`geography'_name name"
			// Recast linked variables
			frunalias 	`geography'  `geography'_name
			}

		if "`data_type'" == "wac" {
			qui fralias add `geography', from(xwalk_w)
			la var `geography' "`geography' code"
			qui fralias add `geography'_name = `geography'name, from(xwalk_w)
			la var `geography'_name "`geography'_name name"
			// Recast linked variables
			qui frunalias 	`geography'  `geography'_name
			}

		// process data
		local work_seg: di lower("`work_seg'") //to match lower case in dataset

		if "`work_seg'" == "" {
			*create varlist to be used
			qui ds c*, has(type numeric)
			local jobs "`r(varlist)'"
			foreach v of local jobs {
			egen job_`v' = sum(`v'), by(`geography' year)
			la var job_`v' "Number of Jobs Category `v'"
			}
		}
		else {
			local jobs `work_seg'
			foreach v of local jobs {
				egen job_`v' = sum(`v'), by(`geography' year)
				la var job_`v' "Number of Jobs Category `v'"
			}
		}

		//retain only key variables
		keep year `geography' `geography'_name job_*

		// Deduplicate to retain geography with Inflow/Outflow results
		if !`geog_poly' {
			**US Census geographies
			qui duplicates drop `geography' year, force
		}
		else {
			**polygon
			qui duplicates drop `geography' year, force
			keep if `geography' == 1
		}

		//process by selected geography
		if "`geo_id'" == "" {
			di as text "{p}no specific geography defined, entire output dataset retained.{p_end}"
			}

		else {
			if "`geo_id'" != "" {
				keep if `geography' == "`geo_id'"
				di as text "{p}Retaining ouput for geography:{result:`geography'} and geography id: {result:`geo_id'}. {p_end}"
				sort year
			}
		}

		// export results ------------------------------------------------------
		if "`saveas'" != "" {
			local fr_opt
			if "`saveframes'" != "" local fr_opt "saveframes(`out_frame')"
			local ex_opt
			if "`exportexcel'" != "" local ex_opt "exportexcel"
			lodes_save_output, saveas(`"`saveas'"') `replace' `ex_opt' `fr_opt'
		}


		//clean up data frames
		if "`deleteframe'" != "" {
			frame change `in_frame'
			frame drop `out_frame'
		}

		//return to main frame
		frame change `in_frame'
		frame list

		// browse if frame retained
		if "`deleteframe'" =="" {
			frame change `out_frame'
			`browse'
		}

end
********************************************************************************
**************************Polygon Process***************************************
********************************************************************************
capture program drop lodes_polygon_process
program lodes_polygon_process
		version 16.1
		args name shapedir

		di as text "{p}Shapefile directory used: {result:`shapedir'}{p_end}"

		// validate inputs
		if "`shapedir'" == "" {
			di as error "{p}You must supply the directory location of the shapefile. Set up using option: {input:{it:shapedir()}}{p_end}"
			exit 111
		}
		if !fileexists("`shapedir'/`name'.zip") {
			di as error `"{p}You must provide the shapefile (zipped) in the `shapedir' directory.{p_end}"'
			exit 111
		}

		di as result "{p}Shapefile `name' exists and processed.{p_end}"

		// Remember the user's working directory and the frame they came in
		// on so we can restore both on any exit path. Previously a successful
		// run did `cd ..' (wrong: it assumed the user was originally in the
		// shapedir's parent), and any mid-flight error left the user stranded
		// in shapedir and/or in the xwalk frame.
		local _orig_pwd  : pwd
		local _entry_frame = c(frame)

		// Wrap the shapefile work in capture-noisily so any error short-
		// circuits to the restore section below (instead of returning to the
		// caller with pwd/frame still pointing at the wrong place).
		capture noisily {
			qui cd "`shapedir'"

			// extract just the .shp and .dbf members of the zip
			foreach f in shp dbf {
				qui unzipfile "`name'", ifilter(".*`f'") replace
			}

			// locate the unzipped .shp file (expect exactly one)
			local _shp_files : dir "." files "*.shp"
			local _shp_count : list sizeof _shp_files
			if `_shp_count' == 0 {
				di as error "{p}No .shp file found after unzipping {result:`name'.zip}.{p_end}"
				exit 601
			}
			if `_shp_count' > 1 {
				di as error "{p}Multiple .shp files found in {result:`name'.zip}; expected exactly one.{p_end}"
				exit 601
			}
			// `: dir' returns each name in double quotes; gettoken strips them
			gettoken shpname _ : _shp_files

			qui spshape2dta `"`shpname'"', replace

			// apply geoinpoly to the LODES crosswalk frame
			local _state = substr("`_entry_frame'", 1, 2)
			frame change `_state'_xwalk

			// remove pre-existing copies of the polygon ID/name variables
			capture confirm variable `name'
			if _rc == 0 qui drop `name'
			capture confirm variable `name'name
			if _rc == 0 qui drop `name'name

			qui destring blklatdd blklondd, replace

			// spshape2dta writes `stem'_shp.dta from `stem'.shp
			local stem = usubinstr(`"`shpname'"', ".shp", "_shp", .)
			qui geoinpoly blklatdd blklondd using "`stem'", unique

			rename _ID `name'
			qui replace `name' = 0 if `name' == .
			qui gen `name'name = "`name'"

			// Clean up the temp files we unzipped/created. Uses Stata's
			// erase (portable) instead of `!del' (Windows-only) so the
			// command also works on macOS and Linux.
			foreach ext in dta shp dbf shx prj sbn sbx {
				local _tmp : dir "." files "*.`ext'"
				foreach f of local _tmp {
					capture erase `f'
				}
			}
		}
		local _rc = _rc

		// Restore the user's working directory and original frame on any
		// exit path (success or error).
		qui cd `"`_orig_pwd'"'
		capture frame change `_entry_frame'

		if `_rc' exit `_rc'
end

********************************************************************************
**************************Data Check***************************************
********************************************************************************
*Per-state year availability for OD and WAC data (LODES8 tech doc v8.4).
*RAC has broader coverage and is not restricted here.
*Flags any requested year that falls outside the available window for the state
*(the previous implementation only fired when *all* requested years were
*unavailable, silently passing partial-overlap requests).
capture program drop lodes_data_check
program lodes_data_check
		version 16.1
		args state data_type years

		// Only OD and WAC have per-state restrictions
		if !inlist("`data_type'", "od", "wac") exit

		// Per-state availability window (start/end inclusive).
		// Update these constants as LODES publishes new years.
		local avail
		if      "`state'" == "ak" local avail "2002/2016"
		else if "`state'" == "ar" local avail "2003/2023"
		else if "`state'" == "az" local avail "2004/2023"
		else if "`state'" == "dc" local avail "2010/2023"
		else if "`state'" == "ma" local avail "2002/2023"
		else if "`state'" == "mi" local avail "2002/2021"
		else if "`state'" == "ms" local avail "2004/2023"
		else if "`state'" == "nh" local avail "2003/2023"
		else if "`state'" == "pr" local avail ""
		else exit  // no known restrictions for this state

		// Expand requested years into a flat numlist
		qui numlist "`years'"
		local req `r(numlist)'

		// Expand the availability window
		local avail_list
		if "`avail'" != "" {
			qui numlist "`avail'"
			local avail_list `r(numlist)'
		}

		// Requested years that are not in the available window
		local unavail : list req - avail_list

		if "`unavail'" != "" {
			local STATE = strupper("`state'")
			local DTYPE = strupper("`data_type'")
			di as error "{p}`DTYPE' data for state `STATE' is not available for year(s): {result:`unavail'}.{p_end}"
			if "`avail'" != "" {
				di as error "{p}Available years for `STATE' `DTYPE': {result:`avail'} (per LODES8 tech doc).{p_end}"
			}
			else {
				di as error "{p}No years currently available for `STATE' `DTYPE'.{p_end}"
			}
			exit 198
		}
end

********************************************************************************
**************************Geography Helper*************************************
********************************************************************************
*Sets c_local is_polygon = 1 if `geog' is NOT one of the standard
*Census-recognised LODES geographies (and is therefore treated as a
*user-supplied polygon name); 0 otherwise. The canonical geography list
*lives only here -- update it in one place if LODES adds new geographies.
capture program drop lodes_is_polygon
program lodes_is_polygon
	version 16.1
	args geog
	local _std_geos "bgrp trct zcta cty stplc cbsa st"
	local _pos : list posof "`geog'" in _std_geos
	c_local is_polygon = !`_pos'
end

********************************************************************************
**************************Input-Frame Validation*******************************
********************************************************************************
*Validates that the current frame was created by `lodes get' with one of
*the allowed data types, and sets up the locals subcommands expect.
*
*Usage:
*    lodes_check_input_frame od        // destination, inflow_outflow
*    lodes_check_input_frame wac rac   // area_profile
*
*Sets c_local: in_frame, data_type, state, xwalk
*Exits with error 198 if the current frame's data_type is not in the list.
capture program drop lodes_check_input_frame
program lodes_check_input_frame
	version 16.1
	// `0' is all positional args (space-separated), e.g. "od" or "wac rac"
	local _in_frame = c(frame)
	local _state = substr("`_in_frame'", 1, 2)

	local _dtype
	foreach _t in `0' {
		local _len = ustrlen("`_t'")
		local _candidate = substr("`_in_frame'", 4, `_len')
		if "`_candidate'" == "`_t'" {
			local _dtype "`_t'"
			continue, break
		}
	}

	if "`_dtype'" == "" {
		di as error `"{p}The current frame ({result:`_in_frame'}) was not created by {bf:lodes get} with data type {result:`0'}. Run {bf:lodes get} with one of those data types first, then re-run this subcommand.{p_end}"'
		exit 198
	}

	c_local in_frame  "`_in_frame'"
	c_local data_type "`_dtype'"
	c_local state     "`_state'"
	c_local xwalk     "`_state'_xwalk"
end

********************************************************************************
**************************Frame-Name Length Check******************************
********************************************************************************
*Errors out if `fr_name' exceeds Stata's 32-character frame-name limit.
*Without this, frame copy / frame create would fail with an opaque
*Stata-internal error; this helper surfaces a clearer cause and remedy.
capture program drop lodes_check_frame_name
program lodes_check_frame_name
	version 16.1
	args fr_name
	if ustrlen("`fr_name'") > 32 {
		di as error `"{p}Generated frame name {bf:`fr_name'} exceeds Stata's 32-character frame-name limit. Use a shorter geography name (or origin specifier) to keep the composite name under the limit.{p_end}"'
		exit 198
	}
end

capture program drop lodes_state_check
program lodes_state_check
	version 16.1
	args url_base state
	// Probe the URL by copying it into a unique tempfile. tempfile names are
	// generated by Stata and auto-erased when the program ends, so this works
	// safely when multiple lodes invocations run concurrently and leaves no
	// debris in c(tmpdir).
	tempfile _lodes_probe
	cap copy "`url_base'" `"`_lodes_probe'"', replace
	if _rc != 0 {
		di as error "{p} State abbreviation {result:`state'} does not exist.{p_end}"
		exit 601
	}
end

********************************************************************************
**************************Save Output Helper***********************************
********************************************************************************
*Shared save/export logic used by lodes_get, lodes_destination,
*lodes_inflow_outflow, and lodes_area_profile.
*
*Always saves the current data as `saveas'.dta. Optionally:
*  - exportexcel       : also writes `saveas'.xlsx
*  - saveframes(list)  : also writes `saveas'.dtas containing the named frames
*                        (caller passes the explicit frame list to save)
*  - replace           : overwrite any of the above if they already exist;
*                        without replace, a pre-existing .xlsx or .dtas errors
capture program drop lodes_save_output
program lodes_save_output
	version 16.1
	syntax , SAVEas(string) [EXportexcel SAVEFrames(string) replace]

	// strip any extension from saveas (.dta, .xlsx, etc.)
	local saveas = ustrregexra("`saveas'", "\.(.*)$", "")

	// save the current data as a Stata dataset
	qui save "`saveas'.dta", `replace'

	// optional Excel export
	if "`exportexcel'" != "" {
		if "`replace'" == "" {
			capture confirm file "`saveas'.xlsx"
			if _rc == 0 {
				di as error "file `saveas'.xlsx already exists"
				exit 602
			}
		}
		qui export excel using "`saveas'.xlsx", firstrow(variables) `replace'
	}

	// optional Stata frames file
	if "`saveframes'" != "" {
		if "`replace'" == "" {
			capture confirm file "`saveas'.dtas"
			if _rc == 0 {
				di as error "file `saveas'.dtas already exists"
				exit 602
			}
		}
		qui frames save "`saveas'", frames(`saveframes') `replace'
	}
end