diff --git a/README.md b/README.md
index c551458..e9fcd58 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ dependencies: [
 func characterIndexes(within rect: CGRect) -> IndexSet
 func enumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void)
 func enumerateLineFragments(in range: NSRange, block: (CGRect, NSRange, inout Bool) -> Void)
+func boundingRect(for range: NSRange) -> NSRect?
 ```
 
 ### `NSTextLayoutManager` Additions
@@ -45,9 +46,17 @@ func enumerateLineFragments(with provider: NSTextElementProvider, block: (NSText
 
 ### `NSTextView`/`UITextView` Additions
 
-```
+```swift
 func characterIndexes(within rect: CGRect) -> IndexSet
 var visibleCharacterIndexes: IndexSet
+func boundingRect(for range: NSRange) -> NSRect?
+```
+
+### `NSRange` and `NSTextRange` Additions
+
+```swift
+NSRange.init?(_ textRange: NSTextRange)
+NSTextRange.init?(_ range: NSRange)
 ```
 
 ## Contributing and Collaboration
diff --git a/Sources/Glyph/NSTextContainer+Additions.swift b/Sources/Glyph/NSTextContainer+Additions.swift
index 9c123da..229b2b1 100644
--- a/Sources/Glyph/NSTextContainer+Additions.swift
+++ b/Sources/Glyph/NSTextContainer+Additions.swift
@@ -6,16 +6,8 @@ import UIKit
 
 #if os(macOS) || os(iOS) || os(visionOS)
 extension NSTextContainer {
-	var nonDowngradingLayoutManager: NSLayoutManager? {
-		if #available(macOS 12.0, iOS 15.0, *), textLayoutManager != nil {
-			return nil
-		}
-
-		return layoutManager
-	}
-
 	private func tk1EnumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) {
-		guard let layoutManager = nonDowngradingLayoutManager else { return }
+		guard let layoutManager = layoutManager else { return }
 
 		let glyphRange = layoutManager.glyphRange(forBoundingRect: rect, in: self)
 
@@ -95,4 +87,30 @@ extension NSTextContainer {
 		tk1EnumerateLineFragments(in: range, block: block)
 	}
 }
+
+extension NSTextContainer {
+	public func boundingRect(for range: NSRange) -> NSRect? {
+		if #available(macOS 12.0, iOS 15.0, *), let textLayoutManager {
+			return textLayoutManager.boundingRect(for: range)
+		}
+
+		return tk1BoundingRect(for: range)
+	}
+
+	private func tk1BoundingRect(for range: NSRange) -> NSRect? {
+		guard let layoutManager else { return nil }
+
+		let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
+
+		return layoutManager.boundingRect(forGlyphRange: glyphRange, in: self)
+	}
+
+	private func tk1TextRange(intersecting rect: CGRect) -> NSRange? {
+		guard let layoutManager else { return nil }
+
+		let glyphRange = layoutManager.glyphRange(forBoundingRect: rect, in: self)
+
+		return layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
+	}
+}
 #endif
diff --git a/Sources/Glyph/NSTextLayoutManager+Additions.swift b/Sources/Glyph/NSTextLayoutManager+Additions.swift
index 907778c..ac9f89c 100644
--- a/Sources/Glyph/NSTextLayoutManager+Additions.swift
+++ b/Sources/Glyph/NSTextLayoutManager+Additions.swift
@@ -64,7 +64,39 @@ extension NSTextLayoutManager {
 		})
 	}
 
-	public func enumerateLineFragments(in range: NSRange, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) {
+	private func enumerateTextLineFragments(
+		in range: NSRange,
+		options: NSTextLayoutFragment.EnumerationOptions = [],
+		block: (NSTextLineFragment, CGRect, NSRange, inout Bool) -> Void
+	) {
+		guard let textContentManager else { return }
+
+		let docStart = documentRange.location
+		guard
+			let start = textContentManager.location(docStart, offsetBy: range.lowerBound),
+			let end = textContentManager.location(docStart, offsetBy: range.upperBound)
+		else {
+			return
+		}
+
+		enumerateTextLayoutFragments(from: start, options: options) { fragment in
+			let fragmentRange = fragment.rangeInElement
+
+			var stop = false
+
+			fragment.enumerateLineFragments(with: textContentManager) { lineFragment, frame, elementRange in
+				block(lineFragment, frame, elementRange, &stop)
+			}
+
+			return stop == false && fragmentRange.endLocation.compare(end) == .orderedAscending
+		}
+	}
+
+	public func enumerateLineFragments(
+		in range: NSRange,
+		options: NSTextLayoutFragment.EnumerationOptions = [],
+		block: (CGRect, NSRange, inout Bool) -> Void
+	) {
 		guard let textContentManager else { return }
 
 		let start = documentRange.location
@@ -85,5 +117,15 @@ extension NSTextLayoutManager {
 			return stop == false && fragmentRange.endLocation.compare(end) == .orderedAscending
 		}
 	}
+
+	func boundingRect(for range: NSRange) -> NSRect? {
+		var rect: NSRect? = nil
+
+		enumerateTextLineFragments(in: range, options: [.ensuresLayout]) { lineFragment, lineRect, lineRange, stop in
+			rect = rect?.union(lineRect) ?? lineRect
+		}
+
+		return rect
+	}
 }
 #endif
diff --git a/Sources/Glyph/NSTextRange+NSRange.swift b/Sources/Glyph/NSTextRange+NSRange.swift
index 89d106e..369045a 100644
--- a/Sources/Glyph/NSTextRange+NSRange.swift
+++ b/Sources/Glyph/NSTextRange+NSRange.swift
@@ -7,6 +7,31 @@ import UIKit
 // Taken from https://github.com/chimeHQ/Rearrange
 
 #if os(macOS) || os(iOS) || os(visionOS)
+@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
+final class UTF16TextLocation: NSObject, NSTextLocation {
+	let value: Int
+
+	init(value: Int) {
+		self.value = value
+	}
+
+	func compare(_ location: any NSTextLocation) -> ComparisonResult {
+		guard let utf16Loc = location as? UTF16TextLocation else {
+			return .orderedSame
+		}
+
+		if value < utf16Loc.value {
+			return .orderedAscending
+		}
+
+		if value > utf16Loc.value {
+			return .orderedDescending
+		}
+
+		return .orderedSame
+	}
+}
+
 @available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
 extension NSRange {
 	init(_ textRange: NSTextRange, provider: NSTextElementProvider) {
@@ -26,5 +51,28 @@ extension NSRange {
 
 		self.init(start..<end)
 	}
+
+	public init?(_ textRange: NSTextRange) {
+		guard
+			let start = textRange.location as? UTF16TextLocation,
+			let end = textRange.endLocation as? UTF16TextLocation
+		else {
+			return nil
+		}
+
+		self.init(start.value..<end.value)
+	}
 }
+
+@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
+@available(watchOS, unavailable)
+extension NSTextRange {
+	public convenience init?(_ range: NSRange) {
+		let start = UTF16TextLocation(value: range.lowerBound)
+		let end = UTF16TextLocation(value: range.upperBound)
+
+		self.init(location: start, end: end)
+	}
+}
+
 #endif
diff --git a/Sources/Glyph/NSTextView+Additions.swift b/Sources/Glyph/NSTextView+Additions.swift
index e589df9..777d8b3 100644
--- a/Sources/Glyph/NSTextView+Additions.swift
+++ b/Sources/Glyph/NSTextView+Additions.swift
@@ -33,5 +33,22 @@ extension TextView {
 	public var visibleCharacterIndexes: IndexSet {
 		characterIndexes(within: visibleContainerRect)
 	}
+
+	/// Returns the bounding rectangle for the given text range.
+	public func boundingRect(for range: NSRange) -> NSRect? {
+#if os(macOS) && !targetEnvironment(macCatalyst)
+		guard let rect = textContainer?.boundingRect(for: range) else {
+			return nil
+		}
+#elseif os(iOS) || os(visionOS)
+		guard let rect = textContainer.boundingRect(for: range) else {
+			return nil
+		}
+#endif
+
+		let origin = textContainerOrigin
+
+		return rect.offsetBy(dx: origin.x, dy: origin.y)
+	}
 }
 #endif