Hello, you have come here looking for the meaning of the word . In DICTIOUS you will not only get to know all the dictionary meanings for the word , but we will also tell you about its etymology, its characteristics and you will know how to say in singular and plural. Everything you need to know about the word you have here. The definition of the word will help you to be more precise and correct when speaking or writing your texts. Knowing the definition of, as well as those of other words, enriches your vocabulary and provides you with more and better linguistic resources.

Implements Template:fi-pronunciation and Template:fi-IPA.


local export = {}

-- used by cartesian_make and cartesian_combine. loaded as necessary
local m_bit32

local m_IPA = require("Module:IPA")
local m_str_utils = require("Module:string utilities")

local find = m_str_utils.find
local gmatch = m_str_utils.gmatch
local gsub = m_str_utils.gsub
local len = m_str_utils.len
local lower = m_str_utils.lower
local match = m_str_utils.match
local sub = m_str_utils.sub
local U = m_str_utils.char
local unpack = unpack or table.unpack -- Lua 5.2 compatibility

local gsub_lookahead = require("Module:gsub lookahead")

local langcode = "fi"

-- spelled vowels
local vowels = "aeiouyåäö"
-- any of 'vowels' as a pattern
local vowel = ""
-- spelled consonants
local consonants = "bcdfghjklmnpqrstvwxzšžʔ"
-- any of 'consonants' as a pattern
local consonant = ""
-- straight apostrophe
local apostrophe = "'"
-- "tertiary stress", a weaker secondary stress (either rhythmic or in some compound words). is there a better way to represent this?
local tertiary = "ˌ"
-- include ˣ because final gemination does not affect rhymes
local ipa_symb = "ˣˈˌ"..tertiary.."̯̝̞̠̪" 

-- orthographic symbols that signify separation of syllables
local sep_symbols = "'’./ +\"-"
-- these signify that the next syllable is an "initial" syllable in a word
-- all symbols from here should also be in sep_symbols
local stress_indicators = "ˈˌ" .. tertiary .. "/ +-"
local stress_indicator = ""

-- <<SYLLABIFICATION START>>

-- syllabification must also consider * a consonant
local syl_consonants = consonants .. "*"
local syl_consonant = ""

-- diphthongs and long vowels
-- 1 = long vowel, 2 = Vi, 3 = VU, 4 = ie/UO
-- 1 and 2 are long vowels/diphthongs everywhere
-- 3 are diphthongs in wordlet-initial syllables or open syllables
-- 4 are diphthongs in wordlet-initial syllables only
local vowel_sequences = {
	 = 1,  = 1,  = 1,  = 1,
	 = 1,  = 1,  = 1,  = 1,
	 = 2,  = 2,              = 2,
	 = 2,  = 2,  = 2,  = 2,
	 = 3,  = 3,  = 3,  = 3,
	 = 3,  = 3,  = 3,  = 3,
	 = 4,  = 4,  = 4
}

-- sonority by consonant (higher number is more sonorous)
local sonorities = {
	 = -1,  = -1,  = -1,  = -1,  = -1,
	 = -2,  = -2,  = -2,
	 = -3,  = -3,  = -3,
	 = -4,  = -4,  = -4,
	 = -5,  = -5,  = -5,
	 = -6,  = -6,  = -6,
}

-- 'native' consonant clusters
local native_consonants = {
	 = true,  = true,  = true,
	                 = true,
	 = true,  = true,  = true,
	 = true,  = true,  = true,
	 = true,  = true,  = true,
	 = true,  = true,  = true,
	 = true,                  = true,
	 = true,  = true,  = true,
}

local function split_by_sonority(cluster)
	local i = len(cluster)
	-- always split two-consonant clusters evenly
	if i <= 2 then
		return i - 1
	end
	
	-- always split 'native' three-consonant clusters to AB.C
	if i == 3 and native_consonants then
		return 2
	end

	local ng = find(cluster, "ng")
	-- never split ng
	if ng then
		return ng + 1 + split_by_sonority(sub(cluster, ng + 2))
	end
	
	local max_sonority = 0
	while i > 1 do
		local sonority = sonorities
		if sonority == nil then return len(cluster) - 1 end
		if sonority >= max_sonority then break end
		max_sonority = sonority
		i = i - 1
	end
	return i
end

-- used for syllabification overrides (any text between these is not syllabified)
local syl_override_start = U(0xEFF0)
local syl_override_end = U(0xEFF1)

local syl_is_consonant = {}
local syl_is_vowel = {}
local syl_is_sep = {}
local syl_is_stressed = {}

for c in gmatch(syl_consonants, ".") do syl_is_consonant = true end
for c in gmatch(vowels, ".") do syl_is_vowel = true end
for c in gmatch(sep_symbols, ".") do syl_is_sep = true end
for c in gmatch(stress_indicators, ".") do syl_is_stressed = true end

-- mode may be one of the following:
--		default: naive hyphenation using Finnish hyphenation rules
--		"dots": use default rules, but add dots between syllable boundaries if allowed by keep_sep_symbols
--		"sonority": split consonant clusters according to the sonority sequencing principle
--				add dots for new splits (i.e. not those incurred by symbols) followed by multiple consonants if keep_sep_symbols allows it.
local function syllabify(word, keep_sep_symbols, mode)
	local result = {} -- output syllables
	local syllable_prefix = nil -- separator symbol if kept
	local syllable_start = 1
	local has_syllable = false
	local cursor = 1
	local found_vowel = false
	local initial_syllable = true -- note: initial in wordlet, not word
	local lower_word = lower(word)

	local function keep_sep(k)
		return (keep_sep_symbols == true or (type(keep_sep_symbols) == "string" and find(keep_sep_symbols, k, 1, true)))
	end

	local dots = ((mode == "dots") and keep_sep(".")) and "." or nil
	local sonority = mode == "sonority"

	local function add_syllable()
		-- add a syllable if not empty
		if has_syllable then
			local syllable_text = sub(word, syllable_start, cursor - 1)
			if syllable_prefix then
				syllable_text = syllable_prefix .. syllable_text
			end
			table.insert(result, syllable_text)

			-- reset state
			syllable_prefix = dots
			initial_syllable = false
			syllable_start = cursor
			has_syllable = false
			found_vowel = false
		end
	end
	
	local len_word = len(word)

	-- prebuild patterns
	local pattern_cc = "^" .. syl_consonant .. "+"
	local pattern_vnc = "^" .. vowel .. "+"
	local pattern_vcv = "^" .. vowel .. "+" .. syl_consonant .. vowel
	local pattern_ccv = pattern_cc .. vowel
	local pattern_sep = "^%(?()%)?"
	local pattern_stressed = "^%(?%)?"

	while cursor <= len_word do
		local symbol = sub(lower_word, cursor, cursor)

		if syl_is_consonant then
			-- C...
			local symbol2 = sub(lower_word, cursor + 1, cursor + 1)

			if syl_is_vowel then
				-- CV: end current syllable if we have found a vowel
				if found_vowel then
					add_syllable()
					found_vowel = false
				end
				cursor = cursor + 1
	
			elseif (sonority and found_vowel and syllable_start < cursor
						and syl_is_consonant and match(lower_word, pattern_ccv, cursor)) then
				-- (V)C+V: split by sonority
				local cluster = match(lower_word, pattern_cc, cursor)
				local split_point = split_by_sonority(cluster)
	
				if split_point ~= nil then
					-- split at the specified position.
					cursor = cursor + split_point
					add_syllable()
	
					local len_cluster = len(cluster)
					-- add a dot if dots are allowed, and if the split point is not before the final consonant
					local add_dot = keep_sep(".") and split_point ~= len_cluster - 1
					if add_dot then syllable_prefix = "." end
					cursor = cursor + len_cluster - split_point
					found_vowel = false
				end

			else
				-- C: continue
				cursor = cursor + 1
			end
			has_syllable = true

		elseif syl_is_vowel then
			if found_vowel then
				-- already found a vowel, end current syllable
				add_syllable()
			end

			local vowel_length = 1
			found_vowel = true
			
			local vv = sub(lower_word, cursor, cursor + 1)
			local rank = vowel_sequences
			if rank then
				-- check for diphthongs or long vowels
				local max_rank
				if initial_syllable then
					max_rank = 4
				else
					-- check for an open syllable
					-- the syllable is open if it is followed by a consonant, except if it is followed by a vowel
					local f1 = sub(lower_word, cursor + 2, cursor + 2)
					local f2 = sub(lower_word, cursor + 3, cursor + 3)
					if syl_is_consonant and not syl_is_vowel then
						-- closed syllable
						max_rank = 2
					else
						-- open syllable
						max_rank = 3
					end
				end

				if rank <= max_rank then
					vowel_length = 2
				end
			end

			cursor = cursor + vowel_length
			has_syllable = true

		else
			local sepchar
			if symbol == "(" then
				-- look for e.g. (.)
				sepchar = match(lower_word, pattern_sep, cursor)
			else
				sepchar = syl_is_sep and symbol
			end

			if sepchar then
				-- separates syllables
				add_syllable()
				
				syllable_prefix = nil
				initial_syllable = syl_is_stressed
				cursor = cursor + (symbol == "(" and 3 or 1)
				if not keep_sep(sepchar) then
					syllable_start = cursor
				end
				found_vowel = false

			elseif symbol == syl_override_start then
				-- override. separate syllables
				if has_syllable then
					add_syllable()
				elseif syllable_start < cursor then
					-- has retained separator character, add to prefix
					syllable_prefix = (syllable_prefix or "") .. sub(lower_word, syllable_start, cursor - 1)
				end

				syllable_start = cursor + (keep_sep(syl_override_start) and 0 or 1)
				local override_end = find(lower_word, syl_override_end, syllable_start)
				if not override_end then error("syl_override_start must end in syl_override_end") end

				has_syllable = true
				if keep_sep(syl_override_end) then
					cursor = override_end + 1
					add_syllable()
				else
					cursor = override_end
					add_syllable()
					cursor = cursor + 1
				end
				syllable_start = cursor

			else
				-- ?: continue
				cursor = cursor + 1
				has_syllable = true
			end
		end
	end

	add_syllable()
	return result
end

export.syllabify = syllabify

-- <<SYLLABIFICATION END>>

-- <<IPA START>>

-- IPA data
-- Finnish vowels in IPA
local ipa_vowels = "ɑeiouyæø"
local ipa_vowel = ""
-- Finnish consonants in IPA
local ipa_consonants = "kptɡgbdfʔsnmŋlrhʋʃʒrjçɦx"
local ipa_consonant = ""
-- IPA diacritics used in transcriptions of Finnish
local ipa_diacritics = "̝̞̠̪"
local ipa_diacritic = ""
local nonsyllabic = U(0x32F) -- inverted breve below
local unreleased = U(0x31A)
local nasalized = U(0x303)
local long = "ː"

-- IPA: either a spelled consonant/vowel or an IPA consonant/vowel
local spelled_consonant = ""
local spelled_vowel = ""

local plosives = "kptbdɡ"
local wordlet_sep = ""

local stress_p = ""
local stress_s = ""
local stress_pd = ""
local stress_sd = ""

local letter_sequence_phonemes = {
	-- ALL FIRST PARTS MUST START WITH ^. longest sequence must come first.
	{ "^ng", "ŋː" },
	{ "^qu", "kʋ" },
	{ "^zz", "ts" },
}

local letters_phonemes = {
	-- symbols outside this list are not changed
	 = "ɑ",
	 = "æ",
	 = "ø",
	 = "o",
	
	 = "k",
	 = "ɡ",
	 = "k",
	 = "ʋ",
	 = "ʃ",
	 = "ʒ",
	
	 = "ks",
	 = "ˣ",
	 = ".",
}

local replacements_narrow = {
	 = "ɑ̝",
	 = "e̞",
	 = "ø̞",
	 = "o̞",
	 = "t̪",
	 = "s̠"
}

--	This adds letters_phonemes = "e", letters_phonemes = "i", etc.
for letter in gmatch("eiouydhfjklmnprstu", ".") do
	letters_phonemes = letter
end

local function apply_post_fixes(p)
	-- initial <gn> is /gn/
	p = gsub(p, "ˈŋn", "ˈɡn")

	-- ŋ is short before consonant (by default)
	p = gsub(p, "ŋ"..long.."("..ipa_consonant..")", "ŋ%1")

	-- dissimilation of vowels by sandhi
	p = gsub(p, "("..ipa_vowel..ipa_diacritic.."*)(?)("..stress_s..")%1", "%1%2%3(ʔ)%1")

	-- C1(.):C2 -> C1(.)(:)C2
	p = gsub(p, "("..ipa_consonant..ipa_diacritic.."*)"..long.."("..ipa_consonant..ipa_diacritic.."*)", "%1("..long..")%2")
	p = gsub(p, "("..ipa_consonant..ipa_diacritic.."*)%.%1("..ipa_consonant..ipa_diacritic.."*)", "%1.(%1)%2")
	p = gsub(p, "("..ipa_consonant..ipa_diacritic.."*)%(%.?%)%1("..ipa_consonant..ipa_diacritic.."*)", "%1(.%1)%2")
	
	return p
end

local function apply_post_fixes_narrow(p)
	-- C1:+C2 -> C1+(C1)C2
	p = gsub(p, "("..ipa_consonant..ipa_diacritic.."*)"..long..tertiary.."("..ipa_consonant..ipa_diacritic.."*)", "%1"..tertiary.."(%1)%2")
	-- C1+C1C2 -> C1+(C1)C2
	-- but C1C2+C2C3 --> C1(C2)+C2C3
	p = gsub(p, "("..ipa_consonant..ipa_diacritic..")("..ipa_consonant..ipa_diacritic.."*)"..tertiary.."(%.?)%2("..ipa_consonant..ipa_diacritic.."*)", "%1(%2)"..tertiary.."%2%3%4")
	p = gsub(p, "("..ipa_consonant..ipa_diacritic.."*)"..tertiary.."(%.?)%1("..ipa_consonant..ipa_diacritic.."*)", "%1"..tertiary.."(%1)%2%3")
	p = gsub(p, "("..ipa_consonant..ipa_diacritic.."*)"..tertiary.."%(?%.?%)%1("..ipa_consonant..ipa_diacritic.."*)", "%1"..tertiary.."(.%1)%2")
	
	-- t is alveolar in /ts/ and /st/
	p = gsub(p, "t̪("..stress_pd.."?%s*)s̠", "t%1s̠")
	p = gsub(p, "s̠("..stress_pd.."?%s*)t̪", "s̠%1t")
	-- n and l become dental in /lt/ /nt/ /tl/ /tn/
	p = gsub(p, "()("..stress_pd.."?%s*)t̪", "%1̪%2t̪")
	p = gsub(p, "t̪("..stress_pd.."?%s*)()", "t̪%1%2̪")

	-- long j, v after i, u diphthong
	p = gsub(p, "(i"..nonsyllabic..")j("..ipa_vowel..")", "%1j("..long..")%2")
	-- /ʋ/ after /u/ usually realized as /w/ (see Suomi, Toivanen and Ylitalo 2008, p. )
	p = gsub(p, "(u"..nonsyllabic..")ʋ("..ipa_vowel..")", "%1w("..long..")%2")
	-- cleanup
	p = gsub(p, "("..stress_s..")%.", "%1")
	p = gsub(p, "("..stress_pd..")"..stress_s, "%1")

    -- tautosyllabic nasals nasalize vowels between them (see Suomi, Toivanen and Ylitalo 2008, p. 22)
	--p = gsub(p, "("..long.."?)("..ipa_vowel..")("..ipa_diacritic.."*)()(.?)", function (n0, nv, nvd, n1, anchor)
	--	-- this cannot be simplified to "(.?)" => "(?)", otherwise a vowel after would match
	--	if not find(anchor, ipa_vowel) then
	--		return n0 .. nv .. nasalized .. nvd .. n1 .. anchor
	--	end
	--end)
	
	-- sandhi: nm > mm, np > mp, nb > mb, nk > ŋk, ng > ŋg
	p = gsub(p, "nm", "m"..long)
	p = gsub(p, "n("..stress_pd.."?%s*)()", "ŋ%1%2")
	p = gsub(p, "n("..stress_pd.."?%s*)()", "m%1%2")
	p = gsub(p, "("..stress_pd.."?%s*)()", "ɱ%1%2")

	-- handle potentially long consonants over secondary stresses
	p = gsub(p, "("..ipa_consonant..ipa_diacritic.."*)%("..long.."%)("..stress_s..")", "%2%1("..long..")")
	p = gsub(p, "("..stress_s..")("..ipa_consonant..ipa_diacritic.."*)%("..long.."%)", "(%2)%1%2")
	p = gsub(p, "("..ipa_diacritic.."*)("..stress_s..")%1", "%1"..unreleased.."%2%1")
	p = gsub(p, "%(("..ipa_diacritic.."*)%)("..stress_s..")%1", "(%1"..unreleased..")%2%1")
	
	--  allophone before front vowels (see Suomi, Toivanen and Ylitalo 2008, p. 27)
	p = gsub(p, "k()", "k̟%1")
	
	return p
end

-- -1 = degenerate
-- 0 = light
-- 1 = heavy with coda
-- 2 = heavy without coda (long vowel/diphthong)
-- 99 = stressed
local class_stressed_syllable = 99
local function classify_syllable(syllable)
	if match(syllable, "^" .. stress_indicator) then
		return class_stressed_syllable
	end
	local cleaned = gsub(syllable, "%(?%*?%)?$", "")
	if match(cleaned, spelled_vowel .. spelled_vowel .. "$") then
		return 2
	elseif match(cleaned, spelled_consonant .. "$") then
		return match(cleaned, spelled_vowel) and 1 or -1
	elseif match(cleaned, spelled_vowel) then
		return 0
	end
	return -1
end

local function has_hiatus(left, right)
	return match(left, spelled_vowel .. "$") and match(right, "^%(?%.?%)?" .. spelled_vowel)
end

local trad_lookahead = 2 -- how many unstressed syllables at most in a single unit, thus max consecutive unstressed syllables
local function add_rhythmic_stress_ltr(hyph, classes)
	-- traditional syllable stress assigning algorithm (e.g. in (V)ISK)
	-- assign from left to right, but shift to later syllables if a light syllable is followed by a heavy syllable

	local add_stress = {}
	local n = #classes
	local prev_stress

	for index, class in ipairs(classes) do
		local stressed = class == class_stressed_syllable
			
		if not stressed and not prev_stress and index < n then
			-- shift stress if current syllable light and a heavy syllable occurs later (except as the last syllable)

			local has_later_heavy_syllable
			local lookahead_end = math.min(index + trad_lookahead, n - 1)
			for i = index + 1, lookahead_end do
				if classes > 0 then
					has_later_heavy_syllable = true
					break
				end
			end

			stressed = index == n - 1 or (class > 0) or not has_later_heavy_syllable
		end
		
		-- check if next syllable already stressed, and do not stress this syllable if so
		if stressed and index < n then
			stressed = stressed and classes ~= class_stressed_syllable
		end

		if index > 1 and stressed then
			add_stress = true
		end
		prev_stress = stressed
	end

	return add_stress
end

local function add_rhythmic_stress_rtl(hyph, classes)
	-- based on Karvonen (2005) "Word prosody in Finnish"
	-- run algorithm for assigning stresses:
	--	(1) analyze each segment started by an already stressed syllable
	--      as a separate prosodic word
	--  (2) assign from *right to left*
	--  (3) final syllable is never stressed (not by this algorithm)
	--  (4) stress may shift back by a single (unstressed) syllable
	--      to a better candidate:
	--			(C)VC is better than (C)V
	--			(C)VV is better than (C)V and (C)VC
	--  (5) if the final syllable in a prosodic word begins with a vowel
	--      and the previous syllable ends in a vowel, skip it

	local add_stress = {}
	local foot = 0
	local word_final = true
	local index = #classes

	while index > 0 do
		local stressed = classes == class_stressed_syllable

		if stressed then
			-- reset
			foot = 0
			word_final = true
		elseif word_final and index > 1 and has_hiatus(hyph, hyph) then
			-- hiatus break at the end of a prosodic word: do nothing

		elseif foot == 0 then
			foot = foot + 1

		-- try to assign a stress.
		-- is the previous syllable stressed?
		elseif index > 1 and classes == class_stressed_syllable then
			-- no two stresses in a row, so do nothing.
		else
			-- we will assign a stress somewhere.
			local class = classes
			-- is this syllable preceded by an unstressed syllable
			-- with more priority?
			if index > 2 and classes ~= class_stressed_syllable and 
							classes > classes then
				-- shift back by one syllable
				index = index - 1
			end

			-- assign stress, and continue.
			add_stress = true
			foot = 0
		end
		index = index - 1
		word_final = false
	end

	return add_stress
end

local function add_rhythmic_stress_collect(hyph, classes, add_stress)
	-- add stress marks
	local rhythmic_stress = tertiary
	local collected = {}
	for index = 1, #hyph do
		if add_stress and classes < class_stressed_syllable then
			local syllable = hyph
			if index > 1 and match(syllable, "^g") and match(hyph, "$") then
				-- make sure .g > ŋ.ŋ
				collected = gsub(collected, "n$", "ŋ")
				syllable = gsub(syllable, "^g", "ŋ")
			end
			collected = rhythmic_stress .. gsub(syllable, "^%.", "")
		else
			collected = hyph
		end
	end
	return table.concat(collected)
end

-- applied *before* IPA conversion
-- returns one result, or possibly two results
local function add_rhythmic_stress(word, assign_rtl)
	-- keep_sep_symbols = true
	local hyph = syllabify(word, true, "sonority")
	local final_heavy = false
	local collected
	local add_rhythmic_stress_impl = assign_rtl and add_rhythmic_stress_rtl or add_rhythmic_stress_ltr

	-- find stressed syllables and classify all syllables
	local classes = {}
	for index, syllable in ipairs(hyph) do
		if index == 1 then
			classes = class_stressed_syllable
		else
			classes = classify_syllable(syllable)
		end
	end

	local add_stress
	-- final heavy syllable may be stressed if preceded by an unstressed
	-- light syllable (in words with at least 3 syllables)
	final_heavy = #classes >= 3 and classes > 0 and classes == 0

	add_stress = add_rhythmic_stress_impl(hyph, classes)

	local res1 = add_rhythmic_stress_collect(hyph, classes, add_stress)
	local res2

	if final_heavy then
		-- make final syllable stressed
		local actual_final = classes
		classes = class_stressed_syllable
		add_stress = add_rhythmic_stress_impl(hyph, classes)
		-- so that add_rhythmic_stress_collect actually adds it
		classes = actual_final
		add_stress = true
		res2 = add_rhythmic_stress_collect(hyph, classes, add_stress)
	end

	return res1, res2
end

local pattern_syllable_c = "^" .. consonant
local pattern_syllable_open = "^" .. consonant .. vowel

local function IPA_handle_diphthongs_replace_open_noninitial(diphthong, after1, after2)
	if syl_is_consonant then
		-- consonant after diphthong
		-- must be followed by vowel so that it's part of the
		-- following syllable, else it's in this syllable
		-- and thus this syllabie is closed

		if syl_is_vowel then
			-- single consonant followed by vowel -> this syllable is open
			return diphthong .. nonsyllabic, after1 .. after2
		else
			-- single consonant not followed by vowel -> this syllable is closed
			return diphthong, after1 .. after2
		end
	end
	if after1 == nonsyllabic then
		-- already marked as nonsyllabic
		return diphthong, after1 .. after2
	end	
	-- no consonant after diphthong -> this syllable is open
	return diphthong .. nonsyllabic, after1 .. after2
end

local pattern_word_initial = "^*"
local pattern_after_sep = stress_indicator .. "*"

local function IPA_handle_diphthongs_word_initial(IPA, diphthong)
	IPA = gsub(IPA, pattern_word_initial .. diphthong, "%0" .. nonsyllabic)
	IPA = gsub(IPA, pattern_after_sep .. diphthong, "%0" .. nonsyllabic)
	return IPA
end

local function IPA_handle_diphthongs_word_initial_or_open(IPA, diphthong)
	IPA = gsub(IPA, pattern_word_initial .. diphthong, "%0" .. nonsyllabic)
	IPA = gsub(IPA, pattern_after_sep .. diphthong, "%0" .. nonsyllabic)
	IPA = gsub_lookahead(IPA, "(" .. diphthong .. ")(.?)(.?)", IPA_handle_diphthongs_replace_open_noninitial)
	return IPA
end

-- Add nonsyllabic diacritic after last vowel of diphthong.
local function IPA_handle_diphthongs(IPA)
	-- /Vi/ everywhere
	IPA = gsub(IPA, "i", "%0" .. nonsyllabic)

	-- /ie/, /uo/, /yö/ root-initially
	IPA = IPA_handle_diphthongs_word_initial(IPA, "ie")
	IPA = IPA_handle_diphthongs_word_initial(IPA, "uo")
	IPA = IPA_handle_diphthongs_word_initial(IPA, "yö")

	-- /VU/ root-initially or in open syllables
	IPA = IPA_handle_diphthongs_word_initial_or_open(IPA, "u")
	IPA = IPA_handle_diphthongs_word_initial_or_open(IPA, "y")
	
	return IPA
end

local function fi_IPA_wordlet(term, is_narrow)
	local cursor = 1
	local len_term = len(term)
	local phonemes = {}
	
	while cursor <= len_term do
		local target
		
		-- try letter_sequence_phonemes first
		for _, replacement in ipairs(letter_sequence_phonemes) do
			if match(term, replacement, cursor) then
				target = replacement
				cursor = cursor + len(replacement) - 1   -- account for ^
				break
			end
		end

		-- then letters_phonemes
		if not target then
			local letter = sub(term, cursor, cursor)
			target = letters_phonemes or letter
			cursor = cursor + 1
		end

		table.insert(phonemes, target or letter)
	end
	
	local result = table.concat(phonemes)
	result = gsub(result, "nk", "ŋk")
	result = gsub(result, "ŋ$", "ŋ")
	
	if is_narrow then
		-- articulation of h (Suomi, Toivanen & Ylitalo 2008, p. 28)
		result = gsub_lookahead(result, "(.?)h(.?)",
			function (before, after)
				local h
				if after ~= "" and after ~= "h" then
					if before ~= "" and (ipa_vowels:find(before) or before == nonsyllabic) then
						if ipa_consonants:find(after) then
							-- vihma, yhtiö
							if before == "i" or before == "y" then
								h = "ç"
							-- mahti, kohme, tuhka
							elseif before == "ɑ" or before == "o" or before == "u" then
								h = "x"
							end
						-- maha
						elseif ipa_vowels:find(after) then
							h = "ɦ"
						end
					end
				end
				
				if h then
					return before .. h, after
				end
			end)
		
		-- double letter replacement must be handled earlier here
		result = gsub(result, "(%a)%1", "%1" .. long)
	
		for letter, phoneme in pairs(replacements_narrow) do
			result = gsub(result, letter, phoneme)
		end
	end
	
	return result
end

local function fi_IPA_termwordlets(term, is_narrow, hyphenstress)
	local wordlet_start = 1
	local wordlet_IPAs = {}

	local split_pattern = "(" .. wordlet_sep .. ")"
	while true do
		local wordlet_sep_at, _, wordlet_sep = find(term, split_pattern, wordlet_start)
		local wordlet_end = wordlet_sep_at and (wordlet_sep_at - 1)
		local part = sub(term, wordlet_start, wordlet_end)

		local stress
		if wordlet_start == 1 then
			stress = "ˈ" -- primary
		elseif wordlet_sep == "/" then
			stress = "ˌ" -- secondary
		else
			stress = hyphenstress
		end

		table.insert(wordlet_IPAs, stress .. fi_IPA_wordlet(part, is_narrow))
		if wordlet_sep_at == nil then
			break
		else
			wordlet_start = wordlet_sep_at + 1
		end
	end

	return wordlet_IPAs
end

-- Generates the broad and optionally also the narrow IPA transcription.
-- If make_narrow is true, the narrow transcription is provided;
-- otherwise it is invalid.
local function fi_IPA_combined_term(term, make_narrow)
	local hyphenstress = "ˌ" -- secondary by default
	local is_prefix, is_suffix

	if find(term, "%/") then
		hyphenstress = tertiary -- tertiary if we have slashes
	end
	
	term = lower(term)
	term, is_suffix = gsub(term, "^%-+", "")
	term, is_prefix = gsub(term, "%-+$", "")

	term = IPA_handle_diphthongs(term)
	
	-- Run IPA for wordlets.
	local IPA = table.concat(fi_IPA_termwordlets(term, false, hyphenstress))
 	
	-- Replace double letters (vowels or consonants) with single letter plus length sign,
	-- and apply post fixes.
	IPA = apply_post_fixes(gsub(IPA, "(%a)%1", "%1" .. long))

	local IPA_narrow
	if make_narrow then
		local term_narrow = add_rhythmic_stress(term)
		IPA_narrow = table.concat(fi_IPA_termwordlets(term_narrow, true, hyphenstress))
		
		-- Handle * in narrow transcription.
		if find(IPA_narrow, "ˣ", nil, true) then
			IPA_narrow = gsub(IPA_narrow, "ˣ(%)?%s*"..stress_p.."?)((.?)" .. ipa_diacritic .. "*)",
				function (post, after, potential_consonant)
					if potential_consonant == "" then
						if find(post, "^%)") then
							return "ʔ" .. post .. after
						else
							return post .. "(ʔ)" .. after
						end
					elseif ipa_consonants:find(potential_consonant) then
						if #post > 0 then
							local amark = ""
							if plosives:find(sub(after, 1, 1)) then
								amark = unreleased
							end
							return after .. amark .. post .. after
						else
							return post .. after .. long
						end
					else
						return post .. "(ʔ)" .. after
					end
				end)
			IPA_narrow = gsub(IPA_narrow, "%(%)", "")
		end

		-- Apply post fixes.
		IPA_narrow = apply_post_fixes_narrow(apply_post_fixes(IPA_narrow))

		if is_prefix > 0 then
			IPA_narrow = IPA_narrow .. "-"
		end
		if is_suffix > 0 then
			IPA_narrow = "-" .. IPA_narrow
		end
	end
	
	if is_prefix > 0 then
		IPA = IPA .. "-"
	end
	if is_suffix > 0 then
		IPA = "-" .. IPA
	end

	return IPA, IPA_narrow
end

local function fi_IPA_term(term, is_narrow)
	local broad, narrow = fi_IPA_combined_term(term, is_narrow)
	return is_narrow and narrow or broad
end

-- exports for tests etc.
export.add_rhythmic_stress = add_rhythmic_stress
export.fi_IPA_term = fi_IPA_term

-- {{fi-IPA}}
function export.fi_IPA_entrypoint(term)
	if type(term) == "table" then
		term = term:getParent().args
	end
	
	local title = mw.title.getCurrentTitle().text
	
	if not term then
		term = title
	elseif term == "*" then
		term = title .. "*"
	end
	
	local no_count = match(term, " ")
	
	IPA_narrow = fi_IPA_term(term, true)
	IPA = fi_IPA_term(term, false)
	return m_IPA.format_IPA_full {
		lang = require("Module:languages").getByCode(langcode),
		items = {{pron = "/" .. IPA .. "/"}, {pron = ""}},
		no_count = no_count,
	}
end

-- <<IPA END>>

local function cleanup_title(x)
	return lower(gsub(x, "–", "-"))
end

-- these two functions are used to build Cartesian products of triplets XAB,
-- such that e.g. (X1, A1, B1), (X2, A2, B2), (X3, _, _)
-- 		creates the combinations
--			X1A1X2A2X3, X1A1X2B2X3, X1B1X2A2X3, X1B1X2B2X3
local function cartesian_make(parts, n)
	local result = parts
	local k = 1
	for i = 2, #parts do
		if m_bit32.band(n, k) > 0 then
			result = result .. parts .. parts
		else
			result = result .. parts .. parts
		end
		k = m_bit32.lshift(k, 1)
	end
	return result
end

local function cartesian_combine(parts)
	m_bit32 = m_bit32 or require("Module:bit32")
	
	local n = m_bit32.lshift(1, #parts - 1)
	local results = {}
	for i = 0, n - 1 do
		table.insert(results, cartesian_make(parts, i))
	end
	return results
end

local function split_by_optional_break(word, only_breaking_diphthongs, prefer_hyphen_first)
	local parts = {}
	local i = 1
	local found = false

	while true do
		local j, je = find(word, "%(%)", i)
		if j == nil then break end
		local allow_break = true
		if only_breaking_diphthongs then
			local prefix = sub(word, i, j - 1)
			local diphthong = sub(word, j - 1, j - 1) .. sub(word, j + 3, j + 3)
			-- if a dot, only when breaks a diphthong
			allow_break = sub(word, j + 1, j + 1) ~= "." or (
				-- never a diphthong if two vowels precede
				not match(prefix, vowel .. vowel .. "$")
				and vowel_sequences)
		end
		if allow_break then
			found = true
			-- prefer dots last if vowels on both sides
			local prev = sub(word, i, j - 1)
			local symbol = sub(word, j + 1, j + 1)
			local prefer_first
			if symbol == "." then
				prefer_first = not (match(prev, vowel .. "$") and match(word, "^" .. vowel, j + 3))
			elseif symbol == "-" then
				prefer_first = prefer_hyphen_first
			end
			if prefer_first then
				table.insert(parts, { prev, symbol, "" })
			else
				table.insert(parts, { prev, "", symbol })
			end
		end
		i = je + 1
	end

	if not found then return { word } end

	table.insert(parts, { sub(word, i), "", "" })

	return cartesian_combine(parts)
end

local function get_autohyphenate_forms(word, title)
	word = gsub(word, "%(%)", "")
	word = gsub(word, "(.)ː", "%1%1")
	word = gsub(word, "", "")
	word = gsub(word, "/", "-")
	word = gsub(word, "^-", "")
	word = gsub(word, "ŋn", "gn")

	if lower(title) == title then
		word = lower(word)
	else
		-- find letters in title and try to match them
		local letters = {}
		for letter in gmatch(title, "%a") do
			table.insert(letters, letter)
		end

		local respelled = ""
		local letter_index = 1

		for character in gmatch(word, ".") do
			if match(character, "%a") then
				local next_letter = letters
				if lower(next_letter) == lower(character) then
					respelled = respelled .. next_letter
					letter_index = letter_index + 1
				else
					respelled = respelled .. character
				end
			else
				respelled = respelled .. character
			end
		end

		word = respelled
	end

	return split_by_optional_break(word, nil, true)
end

local function get_autorhyme_forms(word)
	word = lower(word)
	word = gsub(word, "%(%)", "")
	-- applies gemination mid-word for rhymes
	word = gsub(word, "(" .. consonant .. ")", "%1ː")
	word = gsub(word, "", "")
	-- convert long vowel marks
	word = gsub(word, "(.)ː", "%1%1")
	-- remove IPA symbols and normalize +/ to -
	word = gsub(word, "", "")
	word = gsub(word, "", "-")
	return split_by_optional_break(word, nil, true)
end

local function generate_rhyme(word)
	-- convert syllable weight to hyphen for next routine
	-- (just in case these are included manually... even if they shouldn't be)
	local fmtword = gsub(word, "", "-")
	fmtword = gsub(word, "'", ".")
	
	local sylcount = #syllabify(fmtword, ".")

	-- get final wordlet of a compound word
	local last_hyph = find(fmtword, "%-*$") or 0
	local last_wordlet = sub(fmtword, last_hyph + 1)
	-- use fi-IPA rhythmic stress generator
	local stressed = add_rhythmic_stress(last_wordlet)
	-- find last stress mark and extract everything after that
	local res = match(stressed, ".*" .. stress_indicator .. "(.*)")
	if not res or res == "" then res = last_wordlet end
	
	-- remove initial consonants, convert to IPA, remove IPA symbols
	res = gsub(res, "^%.", "")
	res = gsub(res, "^" .. consonant .. "+", "")
	res = fi_IPA_combined_term(res, false)
	res = gsub(res, "", "")
	res = gsub(res, "^%.", "")
	
	return res, sylcount
end

-- for testing
export.generate_rhyme = generate_rhyme

local function add_trivowel_dots(pron)
	-- find sequences of >= 3 vowels and record their indices
	local toggles = {}
	local scan = 1
	while true do
		local i0, i1 = find(pron, "+", scan)
		if i0 == nil then break end
		table.insert(toggles, i0 + 1)
		table.insert(toggles, i1)
		scan = i1 + 1
	end
	table.insert(toggles, len(pron) + 1)

	-- generate hyphenation, and add dots within multivowel sequences
	local dots = {}
	local hyph = syllabify(pron, true)
	local index = 0
	local recons = ""
	scan = 1
	for _, hpart in ipairs(hyph) do
		index = index + len(hpart)
		while index >= toggles do
			scan = scan + 1
		end
		recons = recons .. hpart
		if scan % 2 == 0 then
			recons = recons .. "."
		end
	end

	return recons
end

local function pron_equal(title, pron)
	if not pron or pron == "" then
		return true
	end
	
	-- handle slashes, pluses and quotes as hyphens
	pron = gsub(pron, '', "-")
	-- remove optional lengthening/shortening/syllable break/gemination, should not cause any issues
	pron = gsub(pron, "%(%)", "")
	-- remove gemination asterisks and syllable separating dots
	pron = gsub(pron, "", "")
	-- map existing glottal stops to apostrophes
	pron = gsub(pron, "%(?ʔ%)?", apostrophe)
	-- /ŋn/ for /gn/ is fine
	pron = gsub(pron, "ŋn", "gn")
	-- remove hyphens but also apostrophes right after hyphens
	-- (so that glottal stop is allowed after hyphen separating two same vowels)
	pron = gsub(pron, "-" .. apostrophe .. "?", "")
	title = gsub(cleanup_title(title), "-", "")
	
	return pron == lower(title)
end

local function pron_equal_special_cases(title, pronunciation)
	if find(title, "ruoa") then
		-- very common exception - support it
		local pronunciation_ruua = gsub(pronunciation, "ruua", "ruoa")
		if pron_equal(title, lower(pronunciation_ruua)) then
			return pronunciation_ruua
		end
	end
	-- fall through to return nil
end

local bracket_delimiters = {
	 = "",
	 = "",
	 = '',
}

local function treat_brackets(pronunciation_with_brackets)
	local result = {}
	local cursor = 1
	
	while true do
		local bracket_start, bracket_end, bracket_text = find(pronunciation_with_brackets, "(%b)", cursor)
		if bracket_start == nil then
			table.insert(result, sub(pronunciation_with_brackets, cursor))
			break
		end

		local delimiter = match(bracket_text, '^.()')
		-- find last slash/hyphen
		local terminate = match(sub(pronunciation_with_brackets, 1, bracket_start), "^.*()" .. bracket_delimiters)
		if cursor > 1 and (not terminate or terminate < cursor) then
			error("bracket {} replacement string had no matching delimiter " .. (delimiter or "-"))
		end
		if terminate then
			table.insert(result, sub(pronunciation_with_brackets, cursor, terminate))
		end

		-- tag replacement with overrides for syllabification
		table.insert(result, syl_override_start .. sub(bracket_text, delimiter and 3 or 2, -2) .. syl_override_end)
		cursor = bracket_end + 1
	end

	return table.concat(result)
end

local function convert_quotes_to(syllabification_cache, text, stress, nostress)
	if find(text, '"') then
		if nostress and stress ~= nostress then
			-- syllabify, and determine which way to go
            -- output nostress if the preceding syllable was stressed,
            -- otherwise output stress

			local syllables = syllabification_cache
			if not syllables then
				syllables = syllabify(text, true)
				syllabification_cache = syllables
			end
			local last_stressed = false
			
			for i, syl in ipairs(syllables) do
				-- first syllable always stressed
				if i == 1 or match(syl, "^" .. stress_indicator) then
					last_stressed = true
				else
					if match(syl, '^"') then
						local next_stressed = i == #syllables or match(syllables, "^" .. stress_indicator)
						syllables = gsub(syl, '^"', (last_stressed or next_stressed) and nostress or stress)
					end
					last_stressed = false
				end
			end
			
			text = table.concat(syllables)
		else
			text = gsub(text, '"', stress)	
		end
	end
	return text
end

local function fi_cleanup_syllabification(syl, all_dots)
	syl = gsub(syl, '', "-")
	syl = gsub(syl, "", ".")
	if all_dots then syl = gsub(syl, "%-", ".") end
	return syl
end

local function fi_hyphenation_from_syllabification(syl)
	syl = fi_cleanup_syllabification(syl)
	
	-- all syllable boundaries are now - or .
	-- first, ban any dots adjacent to a hyphen
	syl = gsub(syl, "%.(-)%-", "%1-")
	syl = gsub(syl, "%-(-)%.", "-%1")
	
	-- then ban any dots that only have a single vowel
	syl = gsub(syl, "^(" .. vowel .. ")%.", "%1")
	syl = gsub(syl, "%.(" .. vowel .. ")$", "%1")
	
	-- in the case x.V.y, technically both xV.y and x.Vy are valid
	-- but here, we only allow xV.y, because there is no way to mark
	-- "allow this or this" without listing all allowed options separately
	-- which can result in exponentially many hyphenations shown
	syl = mw.ustring.gsub(syl, "%.(" .. vowel .. ")%.", "%1.")

	return mw.text.split(syl, "")
end

function export.show(frame)
	local title = mw.title.getCurrentTitle().text
	local lang = require("Module:languages").getByCode(langcode)

	local pronunciation = { "" }
	local ipa = { nil }
	local audio = { }
	local qualifiers = { }
	local rhymes = { nil }
	local syllabifications = { nil }
	local homophones = { }
	local syllabification_labels = { }
	local rhyme_labels = { }
	local homophone_labels = { }
	local nohyphen = false
	local norhymes = false
	local compound_suffix = false
	local categories = { }
	
	if type(frame) == "table" then
		local params = {
			 = { list = true, default = "", allow_holes = true },
			
			 = { list = true, default = nil, allow_holes = true },
			 = { list = true, default = nil, allow_holes = true },  = {},
			 = { list = true, default = nil, allow_holes = true },  = {},
			 = { list = true, default = nil },  = {},
			 = { list = true, default = nil },  = {},
			 = { default = "" },  = {},
			
			 = { list = true, default = nil, allow_holes = true },
			 = { list = true, default = nil, allow_holes = true },
			 = { list = true, default = nil, allow_holes = true },
			 = { list = true, default = nil, allow_holes = true },
			
			 = { type = "boolean", default = false },
			 = { type = "boolean", default = false },
			 = { type = "boolean", default = false },
			
			 = {}, -- for debugging or demonstration only
		}
		
		local args, further = require("Module:parameters").process(frame:getParent().args, params, true)
		
		title = args or title
		pronunciation = args
		ipa = args
		syllabifications = args
		rhymes = args
		qualifiers = args
		syllabification_labels = args
		rhyme_labels = args
		nohyphen = args
		norhymes = args
		compound_suffix = args
		homophones = mw.text.split(args, ",")
		homophone_labels = args
		
		-- hacks; if the first parameter is empty then we want an empty string
		if args and args == nil then args = "" end
		if ipa and ipa == nil then ipa = "" end

        -- clear homophones if empty
		if #homophones == 1 and homophones == "" then homophones = {} end

		if args then syllabifications = args end
		if args then rhymes = args end
		if args then homophones = mw.text.split(args, ",") end
		
		local audios = args
		local captions = args
		if args then audios = args end
		if args then captions = args end
		
		for i, audiofile in ipairs(audios) do
			if audiofile then
				table.insert(audio, {lang = lang, file = audiofile, caption = captions})
			end
		end
	end

	for i, p in ipairs(pronunciation) do
		if p == "" or p == "^" then
			pronunciation = cleanup_title(title)
		elseif p == "*" or p == "(*)" then
			pronunciation = cleanup_title(title) .. p
		elseif find(p, "") then
			p = gsub(p, "t!s", "ts")
			p = gsub(p, "t#s", "ts")
			-- TODO deprecate and remove the two codes below
			p = gsub(p, "%.%!", "(.)")
			p = gsub(p, "%-%!", "-")
			pronunciation = p
		end
	end
	
	-- make sure #pronunciation >= #IPA, because we use #pronunciation
    -- as the source of how many pronunciations we have
	for i, p in ipairs(ipa) do
		if not pronunciation then
			pronunciation = ""
		end
	end
	
    -- whether hyphenation and rhyme data can be generated automatically
	local can_auto_hyphenate = true
    local can_auto_rhyme = true
    -- when generating rhyme or syllabification data automatically, use
    -- model_pronunciation as the source of pronunciation data
	local model_pronunciation

	local i
	-- preprocessing
	local split_optional_breaks = false
	local do_not_split_optional_breaks = false
	local model_pronunciation_index = 1
	local brackets = false
	i = 1
	while i <= #pronunciation do
		-- split (.), but only if there is only one
		local dot_start, dot_end = find(pronunciation, "%(%.%)")
		if dot_start then
			split_optional_breaks = true
			if find(pronunciation, "%(%.%)", dot_end) then
				do_not_split_optional_breaks = true
			end
		end

		-- add syllable break Vii -> V.ii
		if find(pronunciation, "ii") then
			pronunciation = gsub(pronunciation, "()(ii)", "%1.%2")
		end

		-- handle % (long consonant usually pronounced as short)
		if find(pronunciation, "%", 1, true) then
			local original = pronunciation
			local short = gsub(original, "%%", "")
			local long = gsub(original, "(.)%%", "%1%1")
			pronunciation = short
			-- use long consonant in model_pronunciation
			if i == model_pronunciation_index then
				model_pronunciation_index = i + 1
			end
			i = i + 1
			table.insert(pronunciation, i, long)
			do_not_split_optional_breaks = true
		end

		if find(pronunciation, "[", 1, true) then
			brackets = true
		end

        i = i + 1
	end

	local model_pronunciation = pronunciation
	if brackets then
		brackets = model_pronunciation
		-- remove brackets from all pronunciation, keeping only in brackets
		-- for later processing

		for i, p in ipairs(pronunciation) do
			pronunciation = gsub(p, "%b", "")
		end
		model_pronunciation = pronunciation
	end
	
	local model_pronunciation_max_index = model_pronunciation_index
    -- split a single optional break into two pronunciations
    -- model_pronunciation will still have the (.)
    -- syllabification and rhyme logic can handle it
	if #pronunciation == 1 and split_optional_breaks and not do_not_split_optional_breaks then
		local new_pronunciation = {}
		for _, p in ipairs(pronunciation) do
			-- local split_i = split_by_optional_break(p, true)
			local split_i = split_by_optional_break(p)
			for _, np in ipairs(split_i) do
				table.insert(new_pronunciation, np)
			end
		end
		pronunciation = new_pronunciation
		model_pronunciation_max_index = #pronunciation
	end

	local syllabification_cache = {}

	-- generate IPA rows
	local results = {}
	local has_spaces = match(title, " ") or (model_pronunciation and match(model_pronunciation, " "))
	local is_suffix = match(title, "^-")
	local is_affix = not compound_suffix and (match(title, "-$") or is_suffix)
	for i, p in ipairs(pronunciation) do
		local qual = qualifiers or ""
		
		if #qual > 0 then
			qual = " " .. require("Module:qualifier").format_qualifier(qualifiers)
		end
		
		if ipa and ipa ~= "" then
			table.insert(results, "* " .. m_IPA.format_IPA_full {
				lang = lang,
				items = {{pron = ipa}},
				no_count = has_spaces,
			} .. qual)
            -- cannot do automatic rhyme/hyph with IPA pronunciations
            if i <= model_pronunciation_max_index then
				can_auto_hyphenate = false
				can_auto_rhyme = false
			end

		else
            -- do basic cleanup to IPA parameters first:

            -- replace + with - for secondary stress, and colon with long
			if find(p, ":") then p = gsub(p, ":", long) end
			if find(p, "%+") then p = gsub(p, "%+", "-") end

			-- add clarifying dots to three-vowel sequences
			if find(p, "") then
				p = add_trivowel_dots(p)
			end

			-- handle quotes
			if find(p, '"') then
				p = convert_quotes_to(syllabification_cache, p, "-", ".")
			end
			
			local IPA, IPA_narrow = fi_IPA_combined_term(p, true)
			
			-- multi-word stress
			if has_spaces then
				IPA_narrow = gsub(IPA_narrow, " ()", " ˈ%1")
				IPA = gsub(IPA, " ()", " ˈ%1")
			end
			
			-- remove initial stress if suffix
			if is_suffix then
				if compound_suffix then
					IPA_narrow = gsub(IPA_narrow, "^(%-?)ˈ", "%1ˌ")
					IPA = gsub(IPA, "^(%-?)ˈ", "%1ˌ")
				else
					IPA_narrow = gsub(IPA_narrow, "^(%-?)ˈ", "%1")
					IPA = gsub(IPA, "^(%-?)ˈ", "%1")
				end
			end
			
			table.insert(results, "* " .. m_IPA.format_IPA_full {
				lang = lang,
				items = {{pron = "/" .. IPA .. "/"}, {pron = ""}},
				no_count = has_spaces,
			} .. qual)
		end
	end
	
    local should_have_automatic_hyphenation = not (has_spaces or is_affix)
    can_auto_rhyme = can_auto_rhyme and should_have_automatic_hyphenation

    -- terms with spaces, or affixes, never have automatic syllabification/rhymes
	local model_pronunciation_hyphenation
    if not should_have_automatic_hyphenation then
        can_auto_hyphenate = false
    elseif can_auto_hyphenate then
        can_auto_hyphenate = pron_equal(title, lower(model_pronunciation))

		if not can_auto_hyphenate then
			model_pronunciation_hyphenation = model_pronunciation

			if brackets then
				-- check for brackets
				model_pronunciation_hyphenation = treat_brackets(brackets)
				can_auto_hyphenate = pron_equal(title, lower(model_pronunciation_hyphenation))
			end

			if not can_auto_hyphenate then
				-- handle special cases
				local special_cases_checked = pron_equal_special_cases(title, model_pronunciation_hyphenation)
				if special_cases_checked then
					can_auto_hyphenate = true
					model_pronunciation_hyphenation = special_cases_checked
				end
			end
		end
    end
	
	local automatic_syllabifications = false
	if can_auto_hyphenate and not syllabifications then
        -- generate automatic syllabifications
		automatic_syllabifications = true
		local quotes_converted = convert_quotes_to(syllabification_cache, model_pronunciation_hyphenation or model_pronunciation, "(-)")
		local forms = get_autohyphenate_forms(quotes_converted, title)
		local seen_syllabifications = {}
		local syllabification_count = 1
		for _, form in ipairs(forms) do
			if syllabifications then break end
			local syllabified = table.concat(syllabify(form, sep_symbols, "dots"))
			if not seen_syllabifications then
				syllabifications = syllabified
				seen_syllabifications = true
				syllabification_count = syllabification_count + 1
			end
		end
	elseif #syllabifications == 1 and syllabifications == "-" then
		syllabifications = {}
		should_have_automatic_hyphenation = false
	end

	if can_auto_rhyme and not rhymes then
        -- generate automatic rhymes
		local forms = get_autorhyme_forms(convert_quotes_to(syllabification_cache, model_pronunciation, "-", "."))
		for i, form in ipairs(forms) do
			if rhymes then break end
			rhymes = { generate_rhyme(form) }
		end
	elseif #rhymes == 1 and rhymes == "-" then
		rhymes = {}
	end

	for i, a in ipairs(audio) do
		table.insert(results, "* " .. require("Module:audio").format_audio(a))
	end
	
	-- generate rhyme rows from rhyme data
	if not norhymes then
        local syllabification_split_counts = nil

        -- generate rhyme rows
		if #rhymes > 0 then
			-- merge rhymes if they have identical labels
			local last_label = false
			local new_rhymes = {}
			local new_rhyme_labels = {}
			local current_list = {}
			
			for i, r in ipairs(rhymes) do
				local label = rhyme_labels or ""
				if last_label == label then
					table.insert(current_list, r)
				else
					if #current_list > 0 then
						table.insert(new_rhymes, current_list)
					end
					if last_label ~= false then
						table.insert(new_rhyme_labels, last_label)
					end
					current_list = { r }
					last_label = label
				end
			end
			
			table.insert(new_rhymes, current_list)
			table.insert(new_rhyme_labels, last_label)
			rhymes = new_rhymes
			rhyme_labels = new_rhyme_labels
		end
		
        -- add rhymes
		for i, subrhymes in ipairs(rhymes) do
			local label = ""
			if rhyme_labels and #rhyme_labels > 0 then
				label = " " .. require("Module:qualifier").format_qualifier(rhyme_labels)
			end

			if #subrhymes >= 1 then
                -- indexed by rhyme first and syllable count second
				local existing_rhymes = {}
				local seen_rhymes = {}
                -- if at least one rhyme doesn't specify the syllable count,
                -- we will try to determine it automatically
				local must_syllabify = false
                local syllable_count
                -- final syllable counts not passed as part of any rhyme
				local fallback_syllable_counts = nil

				for _, rhyme in ipairs(subrhymes) do
					if type(rhyme) == "table" then
						rhyme, syllable_count = unpack(rhyme)
					elseif find(rhyme, "%/") then
						-- parse syllable count
						rhyme, syllable_count = match(rhyme, "(.+)/(.+)")
						syllable_count = tonumber(syllable_count)
					else
						syllable_count = nil
					end
						
					if syllable_count then
						local existing_rhyme = seen_rhymes
						if not existing_rhyme then
                            -- add the rhyme if not present
							local new_rhyme = { rhyme = rhyme, num_syl = {syllable_count} }
							table.insert(existing_rhymes, new_rhyme)
							seen_rhymes = {  = true, object = new_rhyme }
						elseif not existing_rhyme then
                            -- add the syllable count to an existing rhyme
							table.insert(existing_rhyme.object.num_syl, syllable_count)
							existing_rhyme = true
						end
					else
						must_syllabify = true
						local existing_rhyme = seen_rhymes
						if not existing_rhyme then
							local new_rhyme = { rhyme = rhyme }
							table.insert(existing_rhymes, new_rhyme)
							seen_rhymes = { object = new_rhyme }
						end
					end
				end

				if must_syllabify and (can_auto_hyphenate or not automatic_syllabifications) then
					fallback_syllable_counts = {}
					local seen_syllable_counts = {}
					
					if not manual_syllable_counts then
                        -- generate syllable counts from provided syllabifications
						manual_syllable_counts = {}
						for i, syl in ipairs(syllabifications) do
							if type(syl) == "string" then
								manual_syllable_counts = #mw.text.split(syl, '')
							else
								manual_syllable_counts = #syllabifications
							end
						end
					end
					
					-- get all possible syllable counts from syllabifications
					for i, n in ipairs(manual_syllable_counts) do
						if n > 0 and not seen_syllable_counts then
							table.insert(fallback_syllable_counts, n)
							seen_syllable_counts = true
						end
					end
				end

				table.insert(results, "* " .. require("Module:rhymes").format_rhymes(
					{ lang = lang, rhymes = existing_rhymes, num_syl = fallback_syllable_counts }) .. label)
			end
		end
	end

	-- generate homophone rows from homophone data
	if #homophones > 0 then
		local homophone_rows = {}
		for i, homophone in ipairs(homophones) do
			table.insert(homophone_rows, {
                 = homophone,
                 = homophone_labels and { homophone_labels } or nil
            })
		end
		table.insert(results, "* " .. require("Module:homophones").format_homophones(
			{ lang = lang, homophones = homophone_rows }))
	end

	-- generate syllabification and hyphenation rows from syllabification data
	if not nohyphen and #syllabifications > 0 then
		local syllabification_rows = {}
		local hyphenation_rows = {}
        local seen_syllabifications = {}
        local seen_hyphenations = {}

		for i, syllabification in ipairs(syllabifications) do
			local syllabification_key = fi_cleanup_syllabification(syllabification, true)
			-- deduplicate syllabifications
			local syllabification_row = seen_syllabifications
			if not syllabification_row then
				syllabification_row = { 
					 = m_str_utils.split(syllabification_key, "%."),
					 = nil
				}
				seen_syllabifications = syllabification_row
				table.insert(syllabification_rows, syllabification_row)
			end

            local hyphenation = fi_hyphenation_from_syllabification(syllabification)
			local hyphenation_key = table.concat(hyphenation, ".")

			-- deduplicate hyphenations
			local hyphenation_row = seen_hyphenations
			if not hyphenation_row then
				hyphenation_row = { 
					 = hyphenation,
					 = nil
				}
				seen_hyphenations = hyphenation_row
				table.insert(hyphenation_rows, hyphenation_row)
			end
			
			-- add labels
			if syllabification_labels then
				syllabification_row.qualifiers = syllabification_row.qualifiers or { }
				table.insert(syllabification_row.qualifiers, syllabification_labels)
				
				hyphenation_row.qualifiers = hyphenation_row.qualifiers or { }
				table.insert(hyphenation_row.qualifiers, syllabification_labels)
			end
		end

		table.insert(results, "* " .. require("Module:hyphenation").format_hyphenations(
			{ lang = lang, hyphs = syllabification_rows, caption = "Syllabification<sup>(])</sup>" }))
		table.insert(results, "* " .. require("Module:hyphenation").format_hyphenations(
			{ lang = lang, hyphs = hyphenation_rows, caption = "Hyphenation<sup>(])</sup>" }))
	end
	
	if should_have_automatic_hyphenation and not syllabifications then
		table.insert(categories, "fi-pronunciation missing automatic hyphenation")
	end	
	
	return table.concat(results, "\n") .. require("Module:utilities").format_categories(categories, lang)
end

return export